AI Blog Writer - Implement modular architecture with research, outline, and core services

This commit is contained in:
ajaysi
2025-09-12 16:53:16 +05:30
parent c0a366269d
commit 2ae0c4a8b9
29 changed files with 3210 additions and 907 deletions

View File

@@ -1,5 +1,9 @@
from fastapi import APIRouter, HTTPException
from typing import Any, Dict
import asyncio
import uuid
from datetime import datetime
from loguru import logger
from models.blog_models import (
BlogResearchRequest,
@@ -27,22 +31,234 @@ router = APIRouter(prefix="/api/blog", tags=["AI Blog Writer"])
service = BlogWriterService()
# Simple in-memory task storage (in production, use Redis or database)
task_storage: Dict[str, Dict[str, Any]] = {}
def cleanup_old_tasks():
"""Remove tasks older than 1 hour to prevent memory leaks."""
current_time = datetime.now()
tasks_to_remove = []
for task_id, task_data in task_storage.items():
if (current_time - task_data["created_at"]).total_seconds() > 3600: # 1 hour
tasks_to_remove.append(task_id)
for task_id in tasks_to_remove:
del task_storage[task_id]
@router.get("/health")
async def health() -> Dict[str, Any]:
return {"status": "ok", "service": "ai_blog_writer"}
@router.get("/cache/stats")
async def get_cache_stats() -> Dict[str, Any]:
"""Get research cache statistics."""
try:
from services.cache.research_cache import research_cache
return research_cache.get_cache_stats()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/cache/clear")
async def clear_cache() -> Dict[str, Any]:
"""Clear the research cache."""
try:
from services.cache.research_cache import research_cache
research_cache.clear_cache()
return {"status": "success", "message": "Research cache cleared"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/research/start")
async def start_research(request: BlogResearchRequest) -> Dict[str, Any]:
"""Start a research operation and return a task ID for polling."""
try:
task_id = str(uuid.uuid4())
# Initialize task status
task_storage[task_id] = {
"status": "pending",
"created_at": datetime.now(),
"result": None,
"error": None
}
# Start the research operation in the background
asyncio.create_task(run_research_task(task_id, request))
return {"task_id": task_id, "status": "started"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/research/status/{task_id}")
async def get_research_status(task_id: str) -> Dict[str, Any]:
"""Get the status of a research operation."""
# Cleanup old tasks periodically
cleanup_old_tasks()
if task_id not in task_storage:
raise HTTPException(status_code=404, detail="Task not found")
task = task_storage[task_id]
response = {
"task_id": task_id,
"status": task["status"],
"created_at": task["created_at"].isoformat(),
"progress_messages": task.get("progress_messages", [])
}
if task["status"] == "completed":
response["result"] = task["result"]
elif task["status"] == "failed":
response["error"] = task["error"]
return response
async def run_research_task(task_id: str, request: BlogResearchRequest):
"""Background task to run research and update status with progress messages."""
try:
# Update status to running
task_storage[task_id]["status"] = "running"
task_storage[task_id]["progress_messages"] = []
# Send initial progress message
await _update_progress(task_id, "🔍 Starting research operation...")
# Check cache first
await _update_progress(task_id, "📋 Checking cache for existing research...")
# Run the actual research with progress updates
result = await service.research_with_progress(request, task_id)
# Check if research failed gracefully
if not result.success:
await _update_progress(task_id, f"❌ Research failed: {result.error_message or 'Unknown error'}")
task_storage[task_id]["status"] = "failed"
task_storage[task_id]["error"] = result.error_message or "Research failed"
else:
await _update_progress(task_id, f"✅ Research completed successfully! Found {len(result.sources)} sources and {len(result.search_queries or [])} search queries.")
# Update status to completed
task_storage[task_id]["status"] = "completed"
task_storage[task_id]["result"] = result.dict()
except Exception as e:
await _update_progress(task_id, f"❌ Research failed with error: {str(e)}")
# Update status to failed
task_storage[task_id]["status"] = "failed"
task_storage[task_id]["error"] = str(e)
async def _update_progress(task_id: str, message: str):
"""Update progress message for a task."""
if task_id in task_storage:
if "progress_messages" not in task_storage[task_id]:
task_storage[task_id]["progress_messages"] = []
progress_entry = {
"timestamp": datetime.now().isoformat(),
"message": message
}
task_storage[task_id]["progress_messages"].append(progress_entry)
# Keep only last 10 progress messages to prevent memory bloat
if len(task_storage[task_id]["progress_messages"]) > 10:
task_storage[task_id]["progress_messages"] = task_storage[task_id]["progress_messages"][-10:]
logger.info(f"Progress update for task {task_id}: {message}")
@router.post("/research", response_model=BlogResearchResponse)
async def research(request: BlogResearchRequest) -> BlogResearchResponse:
"""Legacy endpoint - kept for backward compatibility."""
try:
return await service.research(request)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/outline/start")
async def start_outline_generation(request: BlogOutlineRequest) -> Dict[str, Any]:
"""Start an outline generation operation and return a task ID for polling."""
try:
task_id = str(uuid.uuid4())
# Initialize task status
task_storage[task_id] = {
"status": "pending",
"created_at": datetime.now(),
"result": None,
"error": None,
"progress_messages": []
}
# Start the outline generation operation in the background
asyncio.create_task(run_outline_generation_task(task_id, request))
return {"task_id": task_id, "status": "started"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/outline/status/{task_id}")
async def get_outline_status(task_id: str) -> Dict[str, Any]:
"""Get the status of an outline generation operation."""
# Cleanup old tasks periodically
cleanup_old_tasks()
if task_id not in task_storage:
raise HTTPException(status_code=404, detail="Task not found")
task = task_storage[task_id]
response = {
"task_id": task_id,
"status": task["status"],
"created_at": task["created_at"].isoformat(),
"progress_messages": task.get("progress_messages", [])
}
if task["status"] == "completed":
response["result"] = task["result"]
elif task["status"] == "failed":
response["error"] = task["error"]
return response
async def run_outline_generation_task(task_id: str, request: BlogOutlineRequest):
"""Background task to run outline generation and update status with progress messages."""
try:
# Update status to running
task_storage[task_id]["status"] = "running"
task_storage[task_id]["progress_messages"] = []
# Send initial progress message
await _update_progress(task_id, "🧩 Starting outline generation...")
# Run the actual outline generation with progress updates
result = await service.generate_outline_with_progress(request, task_id)
# Update status to completed
await _update_progress(task_id, f"✅ Outline generated successfully! Created {len(result.outline)} sections with {len(result.title_options)} title options.")
task_storage[task_id]["status"] = "completed"
task_storage[task_id]["result"] = result.dict()
except Exception as e:
await _update_progress(task_id, f"❌ Outline generation failed: {str(e)}")
# Update status to failed
task_storage[task_id]["status"] = "failed"
task_storage[task_id]["error"] = str(e)
@router.post("/outline/generate", response_model=BlogOutlineResponse)
async def generate_outline(request: BlogOutlineRequest) -> BlogOutlineResponse:
"""Legacy endpoint - kept for backward compatibility."""
try:
return await service.generate_outline(request)
except Exception as e:
@@ -57,6 +273,42 @@ async def refine_outline(request: BlogOutlineRefineRequest) -> BlogOutlineRespon
raise HTTPException(status_code=500, detail=str(e))
@router.post("/outline/enhance-section")
async def enhance_section(section_data: Dict[str, Any], focus: str = "general improvement"):
"""Enhance a specific section with AI improvements."""
try:
from models.blog_models import BlogOutlineSection
section = BlogOutlineSection(**section_data)
enhanced_section = await service.enhance_section_with_ai(section, focus)
return enhanced_section.dict()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/outline/optimize")
async def optimize_outline(outline_data: Dict[str, Any], focus: str = "general optimization"):
"""Optimize entire outline for better flow, SEO, and engagement."""
try:
from models.blog_models import BlogOutlineSection
outline = [BlogOutlineSection(**section) for section in outline_data.get('outline', [])]
optimized_outline = await service.optimize_outline_with_ai(outline, focus)
return {"outline": [section.dict() for section in optimized_outline]}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/outline/rebalance")
async def rebalance_outline(outline_data: Dict[str, Any], target_words: int = 1500):
"""Rebalance word count distribution across outline sections."""
try:
from models.blog_models import BlogOutlineSection
outline = [BlogOutlineSection(**section) for section in outline_data.get('outline', [])]
rebalanced_outline = service.rebalance_word_counts(outline, target_words)
return {"outline": [section.dict() for section in rebalanced_outline]}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/section/generate", response_model=BlogSectionResponse)
async def generate_section(request: BlogSectionRequest) -> BlogSectionResponse:
try:

View File

@@ -35,6 +35,7 @@ class BlogResearchResponse(BaseModel):
suggested_angles: List[str] = []
search_widget: Optional[str] = None # HTML content for search widget
search_queries: List[str] = [] # Search queries generated by Gemini
error_message: Optional[str] = None # Error message for graceful failures
class BlogOutlineSection(BaseModel):
@@ -51,6 +52,7 @@ class BlogOutlineRequest(BaseModel):
research: BlogResearchResponse
persona: Optional[PersonaInfo] = None
word_count: Optional[int] = 1500
custom_instructions: Optional[str] = None
class BlogOutlineResponse(BaseModel):

View File

@@ -0,0 +1,151 @@
# AI Blog Writer Service Architecture
This directory contains the refactored AI Blog Writer service with a clean, modular architecture.
## 📁 Directory Structure
```
blog_writer/
├── README.md # This file
├── blog_service.py # Main entry point (imports from core)
├── core/ # Core service orchestrator
│ ├── __init__.py
│ └── blog_writer_service.py # Main service coordinator
├── research/ # Research functionality
│ ├── __init__.py
│ ├── research_service.py # Main research orchestrator
│ ├── keyword_analyzer.py # AI-powered keyword analysis
│ ├── competitor_analyzer.py # Competitor intelligence
│ └── content_angle_generator.py # Content angle discovery
├── outline/ # Outline generation
│ ├── __init__.py
│ ├── outline_service.py # Main outline orchestrator
│ ├── outline_generator.py # AI-powered outline generation
│ ├── outline_optimizer.py # Outline optimization
│ └── section_enhancer.py # Section enhancement
├── content/ # Content generation (TODO)
└── optimization/ # SEO & optimization (TODO)
```
## 🏗️ Architecture Overview
### Core Module (`core/`)
- **`BlogWriterService`**: Main orchestrator that coordinates all blog writing functionality
- Provides a unified interface for research, outline generation, and content creation
- Delegates to specialized modules for specific functionality
### Research Module (`research/`)
- **`ResearchService`**: Orchestrates comprehensive research using Google Search grounding
- **`KeywordAnalyzer`**: AI-powered keyword analysis and extraction
- **`CompetitorAnalyzer`**: Competitor intelligence and market analysis
- **`ContentAngleGenerator`**: Strategic content angle discovery
### Outline Module (`outline/`)
- **`OutlineService`**: Manages outline generation, refinement, and optimization
- **`OutlineGenerator`**: AI-powered outline generation from research data
- **`OutlineOptimizer`**: Optimizes outlines for flow, SEO, and engagement
- **`SectionEnhancer`**: Enhances individual sections using AI
## 🔄 Service Flow
1. **Research Phase**: `ResearchService``KeywordAnalyzer` + `CompetitorAnalyzer` + `ContentAngleGenerator`
2. **Outline Phase**: `OutlineService``OutlineGenerator``OutlineOptimizer`
3. **Content Phase**: (TODO) Content generation and optimization
4. **Publishing Phase**: (TODO) Platform integration and publishing
## 🚀 Usage
```python
from services.blog_writer.blog_service import BlogWriterService
# Initialize the service
service = BlogWriterService()
# Research a topic
research_result = await service.research(research_request)
# Generate outline from research
outline_result = await service.generate_outline(outline_request)
# Enhance sections
enhanced_section = await service.enhance_section_with_ai(section, "SEO optimization")
```
## 🎯 Key Benefits
### 1. **Modularity**
- Each module has a single responsibility
- Easy to test, maintain, and extend
- Clear separation of concerns
### 2. **Reusability**
- Components can be used independently
- Easy to swap implementations
- Shared utilities and helpers
### 3. **Scalability**
- New features can be added as separate modules
- Existing modules can be enhanced without affecting others
- Clear interfaces between modules
### 4. **Maintainability**
- Smaller, focused files are easier to understand
- Changes are isolated to specific modules
- Clear dependency relationships
## 🔧 Development Guidelines
### Adding New Features
1. Identify the appropriate module (research, outline, content, optimization)
2. Create new classes following the existing patterns
3. Update the module's `__init__.py` to export new classes
4. Add methods to the appropriate service orchestrator
5. Update the main `BlogWriterService` if needed
### Testing
- Each module should have its own test suite
- Mock external dependencies (AI providers, APIs)
- Test both success and failure scenarios
- Maintain high test coverage
### Error Handling
- Use graceful degradation with fallbacks
- Log errors appropriately
- Return meaningful error messages to users
- Don't let one module's failure break the entire flow
## 📈 Future Enhancements
### Content Module (`content/`)
- Section content generation
- Content optimization and refinement
- Multi-format output (HTML, Markdown, etc.)
### Optimization Module (`optimization/`)
- SEO analysis and recommendations
- Readability optimization
- Performance metrics and analytics
### Integration Module (`integration/`)
- Platform-specific adapters (WordPress, Wix, etc.)
- Publishing workflows
- Content management system integration
## 🔍 Code Quality
- **Type Hints**: All methods use proper type annotations
- **Documentation**: Comprehensive docstrings for all public methods
- **Error Handling**: Graceful failure with meaningful error messages
- **Logging**: Structured logging with appropriate levels
- **Testing**: Unit tests for all major functionality
- **Performance**: Efficient caching and API usage
## 📝 Migration Notes
The original `blog_service.py` has been refactored into this modular structure:
- **Research functionality** → `research/` module
- **Outline generation** → `outline/` module
- **Service orchestration** → `core/` module
- **Main entry point** → `blog_service.py` (now just imports from core)
All existing API endpoints continue to work without changes due to the maintained interface in `BlogWriterService`.

View File

@@ -1,649 +1,11 @@
from typing import Any, Dict, List
from loguru import logger
from services.llm_providers.gemini_provider import gemini_structured_json_response
from models.blog_models import (
BlogResearchRequest,
BlogResearchResponse,
BlogOutlineRequest,
BlogOutlineResponse,
BlogOutlineRefineRequest,
BlogSectionRequest,
BlogSectionResponse,
BlogOptimizeRequest,
BlogOptimizeResponse,
BlogSEOAnalyzeRequest,
BlogSEOAnalyzeResponse,
BlogSEOMetadataRequest,
BlogSEOMetadataResponse,
BlogPublishRequest,
BlogPublishResponse,
ResearchSource,
BlogOutlineSection,
)
class BlogWriterService:
"""Service layer for AI Blog Writer (stub implementations for scaffolding)."""
async def research(self, request: BlogResearchRequest) -> BlogResearchResponse:
"""
Stage 1: Research & Strategy (AI Orchestration)
Uses ONLY Gemini's native Google Search grounding - ONE API call for everything.
Follows LinkedIn service pattern for efficiency and cost optimization.
"""
from services.llm_providers.gemini_grounded_provider import GeminiGroundedProvider
gemini = GeminiGroundedProvider()
topic = request.topic or ", ".join(request.keywords)
industry = request.industry or (request.persona.industry if request.persona and request.persona.industry else "General")
target_audience = getattr(request.persona, 'target_audience', 'General') if request.persona else 'General'
# Single comprehensive research prompt - Gemini handles Google Search automatically
research_prompt = f"""
Research the topic "{topic}" in the {industry} industry for {target_audience} audience. Provide a comprehensive analysis including:
1. Current trends and insights (2024-2025)
2. Key statistics and data points with sources
3. Industry expert opinions and quotes
4. Recent developments and news
5. Market analysis and forecasts
6. Best practices and case studies
7. Keyword analysis: primary, secondary, and long-tail opportunities
8. Competitor analysis: top players and content gaps
9. Content angle suggestions: 5 compelling angles for blog posts
Focus on factual, up-to-date information from credible sources.
Include specific data points, percentages, and recent developments.
Structure your response with clear sections for each analysis area.
"""
# Single Gemini call with native Google Search grounding - no fallbacks
gemini_result = await gemini.generate_grounded_content(
prompt=research_prompt,
content_type="research",
max_tokens=2000
)
# Extract sources from grounding metadata
sources = self._extract_sources_from_grounding(gemini_result)
# Extract search widget and queries for UI display
search_widget = gemini_result.get("search_widget", "") or ""
search_queries = gemini_result.get("search_queries", []) or []
# Parse the comprehensive response for different analysis components
content = gemini_result.get("content", "")
keyword_analysis = self._parse_keyword_analysis(content, request.keywords)
competitor_analysis = self._parse_competitor_analysis(content)
suggested_angles = self._parse_content_angles(content, topic, industry)
logger.info(f"Research completed successfully with {len(sources)} sources and {len(search_queries)} search queries")
return BlogResearchResponse(
success=True,
sources=sources,
keyword_analysis=keyword_analysis,
competitor_analysis=competitor_analysis,
suggested_angles=suggested_angles,
# Add search widget and queries for UI display
search_widget=search_widget if 'search_widget' in locals() else "",
search_queries=search_queries if 'search_queries' in locals() else [],
)
def _extract_sources_from_grounding(self, gemini_result: Dict[str, Any]) -> List[ResearchSource]:
"""Extract sources from Gemini grounding metadata."""
sources = []
# The Gemini grounded provider already extracts sources and puts them in the 'sources' field
raw_sources = gemini_result.get("sources", [])
for src in raw_sources:
source = ResearchSource(
title=src.get("title", "Untitled"),
url=src.get("url", ""),
excerpt=src.get("content", "")[:500] if src.get("content") else f"Source from {src.get('title', 'web')}",
credibility_score=float(src.get("credibility_score", 0.8)),
published_at=str(src.get("publication_date", "2024-01-01"))
)
sources.append(source)
return sources
def _parse_keyword_analysis(self, content: str, original_keywords: List[str]) -> Dict[str, Any]:
"""Parse keyword analysis from the research content."""
# Extract keywords from content sections
lines = content.split('\n')
keyword_section = []
in_keyword_section = False
for line in lines:
if 'keyword' in line.lower() and ('analysis' in line.lower() or 'primary' in line.lower()):
in_keyword_section = True
continue
if in_keyword_section and line.strip():
if line.startswith(('1.', '2.', '3.', '4.', '5.', '6.', '7.', '8.', '9.')):
break
keyword_section.append(line.strip())
return {
"primary": original_keywords[:1] if original_keywords else [],
"secondary": original_keywords[1:] if len(original_keywords) > 1 else [],
"long_tail": [f"{kw} guide" for kw in original_keywords[:2]] if original_keywords else [],
"search_intent": "informational",
"difficulty": 6,
"content_gaps": [f"{kw} best practices" for kw in original_keywords[:2]] if original_keywords else [],
"analysis_content": "\n".join(keyword_section) if keyword_section else content[:200]
}
def _parse_competitor_analysis(self, content: str) -> Dict[str, Any]:
"""Parse competitor analysis from the research content."""
lines = content.split('\n')
competitor_section = []
in_competitor_section = False
for line in lines:
if 'competitor' in line.lower() and ('analysis' in line.lower() or 'top' in line.lower()):
in_competitor_section = True
continue
if in_competitor_section and line.strip():
if line.startswith(('1.', '2.', '3.', '4.', '5.', '6.', '7.', '8.', '9.')):
break
competitor_section.append(line.strip())
return {
"top_competitors": [],
"content_gaps": [],
"opportunities": [],
"analysis_notes": "\n".join(competitor_section) if competitor_section else "Competitor analysis from research"
}
def _parse_content_angles(self, content: str, topic: str, industry: str) -> List[str]:
"""Parse content angles from the research content."""
lines = content.split('\n')
angles_section = []
in_angles_section = False
for line in lines:
if 'angle' in line.lower() and ('suggest' in line.lower() or 'content' in line.lower()):
in_angles_section = True
continue
if in_angles_section and line.strip():
if line.startswith(('1.', '2.', '3.', '4.', '5.', '6.', '7.', '8.', '9.')):
break
if line.strip() and not line.startswith(('', '-', '*')):
angles_section.append(line.strip())
# If no angles found in content, use fallback
if not angles_section:
angles_section = [
f"How {topic} is Transforming {industry}",
f"Latest {topic} Trends: What You Need to Know",
f"{topic} Best Practices for {industry}",
f"Case Study: {topic} Success Stories",
f"The Future of {topic} in {industry}"
]
return angles_section[:5] # Return top 5 angles
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
"""
# 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')
# Build sophisticated outline generation prompt with advanced content strategy
outline_prompt = 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'}
RESEARCH INTELLIGENCE:
Primary Keywords: {', '.join(primary_keywords)}
Secondary Keywords: {', '.join(secondary_keywords)}
Long-tail Opportunities: {', '.join(research.keyword_analysis.get('long_tail', [])[:5])}
Content Angles Discovered:
{chr(10).join([f"{angle}" for angle in content_angles[:6]])}
Research Sources Available: {len(sources)} authoritative sources with current data
STRATEGIC OUTLINE REQUIREMENTS:
1. CONTENT ARCHITECTURE:
- Create 5-7 sections that follow a logical progression
- Each section must have a clear purpose and value proposition
- Build a narrative arc that keeps readers engaged throughout
- Include strategic content gaps that competitors miss
2. SEO OPTIMIZATION:
- Naturally integrate primary keywords in H2 headings (not forced)
- Use secondary keywords in subheadings and key points
- Include long-tail keywords in natural language
- Optimize for featured snippets and voice search
- Create semantic keyword clusters
3. READER ENGAGEMENT:
- Start with a compelling hook that addresses pain points
- Use storytelling elements and real-world examples
- Include actionable insights readers can implement immediately
- Create sections that encourage social sharing
- End with a strong call-to-action
4. CONTENT DEPTH:
- Each section: 2-4 specific, actionable subheadings
- Each section: 4-6 key points with research-backed insights
- Include data points, statistics, and case studies where relevant
- Address common objections and questions
- Provide unique angles not covered by competitors
5. WORD COUNT DISTRIBUTION:
- Introduction: 10-15% of total words
- Main sections: 70-80% of total words (distributed strategically)
- Conclusion: 10-15% of total words
- Total target: {request.word_count or 1500} words
6. COMPETITIVE ADVANTAGE:
- Include fresh perspectives from recent research
- Address emerging trends and future implications
- Provide deeper insights than surface-level content
- Include practical tools, frameworks, or templates
- Reference authoritative sources and data
TITLE STRATEGY:
Create 3 distinct title options that:
- Include primary keywords naturally
- Promise clear value to readers
- Create curiosity and urgency
- Are optimized for click-through rates
- Work well for social media sharing
CRITICAL: Respond ONLY with valid JSON. No additional text or explanations.
JSON FORMAT:
{{
"title_options": [
"Compelling title with primary keyword and benefit",
"Question-based title that creates curiosity",
"How-to title with specific outcome promise"
],
"outline": [
{{
"heading": "Strategic section title with primary keyword",
"subheadings": [
"Specific, actionable subheading 1",
"Data-driven subheading 2",
"Case study or example subheading 3"
],
"key_points": [
"Research-backed insight with specific data",
"Actionable step readers can take immediately",
"Common mistake to avoid with explanation",
"Advanced tip that provides competitive advantage",
"Real-world example or case study"
],
"target_words": 300,
"keywords": ["primary keyword", "secondary keyword", "long-tail phrase"]
}}
]
}}
"""
logger.info("Generating AI-powered outline using research results")
# Define the schema for structured JSON response
outline_schema = {
"type": "object",
"properties": {
"title_options": {
"type": "array",
"items": {"type": "string"},
"description": "3 SEO-optimized title options"
},
"outline": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"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"},
"description": "Keywords to focus on in this section"
}
},
"required": ["id", "heading", "subheadings", "key_points", "word_count", "keywords"]
}
}
},
"required": ["title_options", "outline"]
}
# Generate outline using structured JSON response (no grounding needed)
outline_data = gemini_structured_json_response(
prompt=outline_prompt,
schema=outline_schema,
temperature=0.3,
max_tokens=3000
)
# Check for errors in the response
if isinstance(outline_data, dict) and 'error' in outline_data:
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):
logger.error(f"Invalid outline structure: {outline_data}")
raise ValueError("Invalid outline structure in Gemini response")
# Convert 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:
logger.warning(f"Skipping invalid section data at index {i}")
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[:2] if i < 2 else [], # Assign sources to first 2 sections
target_words=section_data.get('target_words', 300),
keywords=section_data.get('keywords', [])
)
outline_sections.append(section)
title_options = outline_data.get('title_options', [])
if not title_options:
raise ValueError("No title options provided in Gemini response")
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 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 generate_section(self, request: BlogSectionRequest) -> BlogSectionResponse:
# TODO: Generate section markdown incorporating references and persona/tone
md = f"## {request.section.heading}\n\nThis section content will be generated here.\n"
return BlogSectionResponse(success=True, markdown=md, citations=request.section.references)
async def optimize_section(self, request: BlogOptimizeRequest) -> BlogOptimizeResponse:
# TODO: Run readability/EEAT optimization and return diff
return BlogOptimizeResponse(success=True, optimized=request.content, diff_preview=None)
async def hallucination_check(self, payload: Dict[str, Any]) -> Dict[str, Any]:
"""Run hallucination detection on provided text using existing detector service."""
text = str(payload.get("text", "") or "").strip()
if not text:
return {"success": False, "error": "No text provided"}
# Prefer direct service use over HTTP proxy
try:
from services.hallucination_detector import HallucinationDetector
detector = HallucinationDetector()
result = await detector.detect_hallucinations(text)
# Serialize dataclass-like result to dict
claims = []
for c in result.claims:
claims.append({
"text": c.text,
"confidence": c.confidence,
"assessment": c.assessment,
"supporting_sources": c.supporting_sources,
"refuting_sources": c.refuting_sources,
"reasoning": c.reasoning,
})
return {
"success": True,
"overall_confidence": result.overall_confidence,
"total_claims": result.total_claims,
"supported_claims": result.supported_claims,
"refuted_claims": result.refuted_claims,
"insufficient_claims": result.insufficient_claims,
"timestamp": result.timestamp,
"claims": claims,
}
except Exception as e:
return {"success": False, "error": str(e)}
async def seo_analyze(self, request: BlogSEOAnalyzeRequest) -> BlogSEOAnalyzeResponse:
"""Wrap existing SEO tools to produce unified analysis for blog content."""
from services.seo_tools.on_page_seo_service import OnPageSEOService
from services.seo_tools.image_alt_service import ImageAltService
from services.seo_tools.content_strategy_service import ContentStrategyService
content = request.content or ""
target_keywords = request.keywords or []
# On-page analysis (treat content as a virtual URL/document for now)
on_page = OnPageSEOService()
on_page_result = await on_page.analyze_on_page_seo(url="about:blank", target_keywords=target_keywords)
# Image alt coverage (placeholder: no images in raw content yet)
try:
image_alt_service = ImageAltService()
image_alt_status = {"total_images": 0, "missing_alt": 0}
except Exception:
image_alt_status = {"total_images": 0, "missing_alt": 0}
# Strategy hints (keywords/topics)
try:
strategy = ContentStrategyService()
strategy_hints = await strategy.analyze_content_topics(content=content)
except Exception:
strategy_hints = {"topics": [], "gaps": []}
# Lightweight markdown parsing for headings/links/keywords
import re
content_text = content or ""
words = re.findall(r"[A-Za-z0-9']+", content_text)
total_words = max(len(words), 1)
heading_lines = content_text.splitlines()
h1 = sum(1 for ln in heading_lines if ln.startswith('# '))
h2 = sum(1 for ln in heading_lines if ln.startswith('## '))
h3 = sum(1 for ln in heading_lines if ln.startswith('### '))
md_links = re.findall(r"\[([^\]]+)\]\(([^)]+)\)", content_text)
external_links = [u for (_t, u) in md_links if u.startswith('http')]
# Keyword density
density_map: Dict[str, Any] = {"target_keywords": target_keywords}
for kw in target_keywords:
try:
occurrences = len(re.findall(re.escape(kw), content_text, flags=re.IGNORECASE))
except re.error:
occurrences = 0
density_map[kw] = {
"occurrences": occurrences,
"density": round(occurrences / total_words, 4)
}
# Build unified response
recommendations: List[str] = []
if isinstance(on_page_result.get("recommendations"), list):
recommendations.extend(on_page_result["recommendations"])
if strategy_hints.get("gaps"):
recommendations.append("Cover missing topics: " + ", ".join(strategy_hints["gaps"]))
if not external_links:
recommendations.append("Add at least one credible external link to authoritative sources.")
if h2 < 2:
recommendations.append("Increase number of H2 sections for better structure.")
# Internal link suggestions: generate anchors for H2s and propose cross-links
def to_anchor(h: str) -> str:
import re
a = re.sub(r"[^a-z0-9\s-]", "", h.lower())
a = re.sub(r"\s+", "-", a).strip('-')
return a
h2_headings = [ln[3:].strip() for ln in heading_lines if ln.startswith('## ')]
anchors = [to_anchor(h) for h in h2_headings]
internal_link_suggestions = []
for i in range(len(anchors)-1):
internal_link_suggestions.append({
"from": h2_headings[i],
"to": h2_headings[i+1],
"anchor": f"#{anchors[i+1]}",
"suggestion": f"Add internal link from '{h2_headings[i]}' to '{h2_headings[i+1]}'"
})
return BlogSEOAnalyzeResponse(
success=True,
seo_score=float(on_page_result.get("overall_score", 75)),
density=density_map,
structure={
**on_page_result.get("heading_structure", {}),
"markdown_headings": {"h1": h1, "h2": h2, "h3": h3},
"links": {"total": len(md_links), "external": len(external_links)}
},
readability=on_page_result.get("content_analysis", {}),
link_suggestions=([{"suggestion": "Add external citation links for key claims."}] if not external_links else []) + internal_link_suggestions,
image_alt_status=image_alt_status,
recommendations=recommendations,
)
async def seo_metadata(self, request: BlogSEOMetadataRequest) -> BlogSEOMetadataResponse:
# TODO: Generate SEO metadata using existing services
return BlogSEOMetadataResponse(
success=True,
title_options=[request.title or "Generated SEO Title"],
meta_descriptions=["Compelling meta description..."],
open_graph={"title": request.title or "OG Title", "image": ""},
twitter_card={"card": "summary_large_image"},
schema={"@type": "Article"},
)
async def publish(self, request: BlogPublishRequest) -> BlogPublishResponse:
# TODO: Call Wix/WordPress adapters to publish
return BlogPublishResponse(success=True, platform=request.platform, url="https://example.com/post")
"""
AI Blog Writer Service - Main entry point for blog writing functionality.
This module provides a clean interface to the modular blog writer services.
The actual implementation has been refactored into specialized modules:
- research/ - Research and keyword analysis
- outline/ - Outline generation and optimization
- core/ - Main service orchestrator
"""
from .core import BlogWriterService

View File

@@ -0,0 +1,11 @@
"""
Core module for AI Blog Writer.
This module contains the main service orchestrator and shared utilities.
"""
from .blog_writer_service import BlogWriterService
__all__ = [
'BlogWriterService'
]

View File

@@ -0,0 +1,233 @@
"""
Blog Writer Service - Main orchestrator for AI Blog Writer.
Coordinates research, outline generation, content creation, and optimization.
"""
from typing import Dict, Any, List
from loguru import logger
from models.blog_models import (
BlogResearchRequest,
BlogResearchResponse,
BlogOutlineRequest,
BlogOutlineResponse,
BlogOutlineRefineRequest,
BlogSectionRequest,
BlogSectionResponse,
BlogOptimizeRequest,
BlogOptimizeResponse,
BlogSEOAnalyzeRequest,
BlogSEOAnalyzeResponse,
BlogSEOMetadataRequest,
BlogSEOMetadataResponse,
BlogPublishRequest,
BlogPublishResponse,
BlogOutlineSection,
)
from ..research import ResearchService
from ..outline import OutlineService
class BlogWriterService:
"""Main service orchestrator for AI Blog Writer functionality."""
def __init__(self):
self.research_service = ResearchService()
self.outline_service = OutlineService()
# Research Methods
async def research(self, request: BlogResearchRequest) -> BlogResearchResponse:
"""Conduct comprehensive research using Google Search grounding."""
return await self.research_service.research(request)
async def research_with_progress(self, request: BlogResearchRequest, task_id: str) -> BlogResearchResponse:
"""Conduct research with real-time progress updates."""
return await self.research_service.research_with_progress(request, task_id)
# Outline Methods
async def generate_outline(self, request: BlogOutlineRequest) -> BlogOutlineResponse:
"""Generate AI-powered outline from research data."""
return await self.outline_service.generate_outline(request)
async def generate_outline_with_progress(self, request: BlogOutlineRequest, task_id: str) -> BlogOutlineResponse:
"""Generate outline with real-time progress updates."""
return await self.outline_service.generate_outline_with_progress(request, task_id)
async def refine_outline(self, request: BlogOutlineRefineRequest) -> BlogOutlineResponse:
"""Refine outline with HITL operations."""
return await self.outline_service.refine_outline(request)
async def enhance_section_with_ai(self, section: BlogOutlineSection, focus: str = "general improvement") -> BlogOutlineSection:
"""Enhance a section using AI."""
return await self.outline_service.enhance_section_with_ai(section, focus)
async def optimize_outline_with_ai(self, outline: List[BlogOutlineSection], focus: str = "general optimization") -> List[BlogOutlineSection]:
"""Optimize entire outline for better flow and SEO."""
return await self.outline_service.optimize_outline_with_ai(outline, focus)
def rebalance_word_counts(self, outline: List[BlogOutlineSection], target_words: int) -> List[BlogOutlineSection]:
"""Rebalance word count distribution across sections."""
return self.outline_service.rebalance_word_counts(outline, target_words)
# Content Generation Methods (TODO: Extract to content module)
async def generate_section(self, request: BlogSectionRequest) -> BlogSectionResponse:
"""Generate section content from outline."""
# TODO: Move to content module
md = f"## {request.section.heading}\n\nThis section content will be generated here.\n"
return BlogSectionResponse(success=True, markdown=md, citations=request.section.references)
async def optimize_section(self, request: BlogOptimizeRequest) -> BlogOptimizeResponse:
"""Optimize section content for readability and SEO."""
# TODO: Move to optimization module
return BlogOptimizeResponse(success=True, optimized=request.content, diff_preview=None)
# SEO and Analysis Methods (TODO: Extract to optimization module)
async def hallucination_check(self, payload: Dict[str, Any]) -> Dict[str, Any]:
"""Run hallucination detection on provided text."""
text = str(payload.get("text", "") or "").strip()
if not text:
return {"success": False, "error": "No text provided"}
# Prefer direct service use over HTTP proxy
try:
from services.hallucination_detector import HallucinationDetector
detector = HallucinationDetector()
result = await detector.detect_hallucinations(text)
# Serialize dataclass-like result to dict
claims = []
for c in result.claims:
claims.append({
"text": c.text,
"confidence": c.confidence,
"assessment": c.assessment,
"supporting_sources": c.supporting_sources,
"refuting_sources": c.refuting_sources,
"reasoning": c.reasoning,
})
return {
"success": True,
"overall_confidence": result.overall_confidence,
"total_claims": result.total_claims,
"supported_claims": result.supported_claims,
"refuted_claims": result.refuted_claims,
"insufficient_claims": result.insufficient_claims,
"timestamp": result.timestamp,
"claims": claims,
}
except Exception as e:
return {"success": False, "error": str(e)}
async def seo_analyze(self, request: BlogSEOAnalyzeRequest) -> BlogSEOAnalyzeResponse:
"""Analyze content for SEO optimization."""
from services.seo_tools.on_page_seo_service import OnPageSEOService
from services.seo_tools.image_alt_service import ImageAltService
from services.seo_tools.content_strategy_service import ContentStrategyService
content = request.content or ""
target_keywords = request.keywords or []
# On-page analysis (treat content as a virtual URL/document for now)
on_page = OnPageSEOService()
on_page_result = await on_page.analyze_on_page_seo(url="about:blank", target_keywords=target_keywords)
# Image alt coverage (placeholder: no images in raw content yet)
try:
image_alt_service = ImageAltService()
image_alt_status = {"total_images": 0, "missing_alt": 0}
except Exception:
image_alt_status = {"total_images": 0, "missing_alt": 0}
# Strategy hints (keywords/topics)
try:
strategy = ContentStrategyService()
strategy_hints = await strategy.analyze_content_topics(content=content)
except Exception:
strategy_hints = {"topics": [], "gaps": []}
# Lightweight markdown parsing for headings/links/keywords
import re
content_text = content or ""
words = re.findall(r"[A-Za-z0-9']+", content_text)
total_words = max(len(words), 1)
heading_lines = content_text.splitlines()
h1 = sum(1 for ln in heading_lines if ln.startswith('# '))
h2 = sum(1 for ln in heading_lines if ln.startswith('## '))
h3 = sum(1 for ln in heading_lines if ln.startswith('### '))
md_links = re.findall(r"\[([^\]]+)\]\(([^)]+)\)", content_text)
external_links = [u for (_t, u) in md_links if u.startswith('http')]
# Keyword density
density_map: Dict[str, Any] = {"target_keywords": target_keywords}
for kw in target_keywords:
try:
occurrences = len(re.findall(re.escape(kw), content_text, flags=re.IGNORECASE))
except re.error:
occurrences = 0
density_map[kw] = {
"occurrences": occurrences,
"density": round(occurrences / total_words, 4)
}
# Build unified response
recommendations: List[str] = []
if isinstance(on_page_result.get("recommendations"), list):
recommendations.extend(on_page_result["recommendations"])
if strategy_hints.get("gaps"):
recommendations.append("Cover missing topics: " + ", ".join(strategy_hints["gaps"]))
if not external_links:
recommendations.append("Add at least one credible external link to authoritative sources.")
if h2 < 2:
recommendations.append("Increase number of H2 sections for better structure.")
# Internal link suggestions: generate anchors for H2s and propose cross-links
def to_anchor(h: str) -> str:
import re
a = re.sub(r"[^a-z0-9\s-]", "", h.lower())
a = re.sub(r"\s+", "-", a).strip('-')
return a
h2_headings = [ln[3:].strip() for ln in heading_lines if ln.startswith('## ')]
anchors = [to_anchor(h) for h in h2_headings]
internal_link_suggestions = []
for i in range(len(anchors)-1):
internal_link_suggestions.append({
"from": h2_headings[i],
"to": h2_headings[i+1],
"anchor": f"#{anchors[i+1]}",
"suggestion": f"Add internal link from '{h2_headings[i]}' to '{h2_headings[i+1]}'"
})
return BlogSEOAnalyzeResponse(
success=True,
seo_score=float(on_page_result.get("overall_score", 75)),
density=density_map,
structure={
**on_page_result.get("heading_structure", {}),
"markdown_headings": {"h1": h1, "h2": h2, "h3": h3},
"links": {"total": len(md_links), "external": len(external_links)}
},
readability=on_page_result.get("content_analysis", {}),
link_suggestions=([{"suggestion": "Add external citation links for key claims."}] if not external_links else []) + internal_link_suggestions,
image_alt_status=image_alt_status,
recommendations=recommendations,
)
async def seo_metadata(self, request: BlogSEOMetadataRequest) -> BlogSEOMetadataResponse:
"""Generate SEO metadata for content."""
# TODO: Move to optimization module
return BlogSEOMetadataResponse(
success=True,
title_options=[request.title or "Generated SEO Title"],
meta_descriptions=["Compelling meta description..."],
open_graph={"title": request.title or "OG Title", "image": ""},
twitter_card={"card": "summary_large_image"},
schema={"@type": "Article"},
)
async def publish(self, request: BlogPublishRequest) -> BlogPublishResponse:
"""Publish content to specified platform."""
# TODO: Move to content module
return BlogPublishResponse(success=True, platform=request.platform, url="https://example.com/post")

View 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'
]

View 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"
]

View 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

View 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)

View 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

View File

@@ -0,0 +1,21 @@
"""
Research module for AI Blog Writer.
This module handles all research-related functionality including:
- Google Search grounding integration
- Keyword analysis and competitor research
- Content angle discovery
- Research caching and optimization
"""
from .research_service import ResearchService
from .keyword_analyzer import KeywordAnalyzer
from .competitor_analyzer import CompetitorAnalyzer
from .content_angle_generator import ContentAngleGenerator
__all__ = [
'ResearchService',
'KeywordAnalyzer',
'CompetitorAnalyzer',
'ContentAngleGenerator'
]

View File

@@ -0,0 +1,71 @@
"""
Competitor Analyzer - AI-powered competitor analysis for research content.
Extracts competitor insights and market intelligence from research content.
"""
from typing import Dict, Any
from loguru import logger
class CompetitorAnalyzer:
"""Analyzes competitors and market intelligence from research content."""
def analyze(self, content: str) -> Dict[str, Any]:
"""Parse comprehensive competitor analysis from the research content using AI."""
competitor_prompt = f"""
Analyze the following research content and extract competitor insights:
Research Content:
{content[:3000]}
Extract and analyze:
1. Top competitors mentioned (companies, brands, platforms)
2. Content gaps (what competitors are missing)
3. Market opportunities (untapped areas)
4. Competitive advantages (what makes content unique)
5. Market positioning insights
6. Industry leaders and their strategies
Respond with JSON:
{{
"top_competitors": ["competitor1", "competitor2"],
"content_gaps": ["gap1", "gap2"],
"opportunities": ["opportunity1", "opportunity2"],
"competitive_advantages": ["advantage1", "advantage2"],
"market_positioning": "positioning insights",
"industry_leaders": ["leader1", "leader2"],
"analysis_notes": "Comprehensive competitor analysis summary"
}}
"""
from services.llm_providers.gemini_provider import gemini_structured_json_response
competitor_schema = {
"type": "object",
"properties": {
"top_competitors": {"type": "array", "items": {"type": "string"}},
"content_gaps": {"type": "array", "items": {"type": "string"}},
"opportunities": {"type": "array", "items": {"type": "string"}},
"competitive_advantages": {"type": "array", "items": {"type": "string"}},
"market_positioning": {"type": "string"},
"industry_leaders": {"type": "array", "items": {"type": "string"}},
"analysis_notes": {"type": "string"}
},
"required": ["top_competitors", "content_gaps", "opportunities", "competitive_advantages", "market_positioning", "industry_leaders", "analysis_notes"]
}
competitor_analysis = gemini_structured_json_response(
prompt=competitor_prompt,
schema=competitor_schema,
temperature=0.3,
max_tokens=1000
)
if isinstance(competitor_analysis, dict) and 'error' not in competitor_analysis:
return competitor_analysis
else:
# Fail gracefully - no fallback data
logger.error(f"AI competitor analysis failed: {competitor_analysis}")
raise ValueError(f"Competitor analysis failed: {competitor_analysis.get('error', 'Unknown error')}")

View File

@@ -0,0 +1,79 @@
"""
Content Angle Generator - AI-powered content angle discovery.
Generates strategic content angles from research content for blog posts.
"""
from typing import List
from loguru import logger
class ContentAngleGenerator:
"""Generates strategic content angles from research content."""
def generate(self, content: str, topic: str, industry: str) -> List[str]:
"""Parse strategic content angles from the research content using AI."""
angles_prompt = f"""
Analyze the following research content and create strategic content angles for: {topic} in {industry}
Research Content:
{content[:3000]}
Create 7 compelling content angles that:
1. Leverage current trends and data from the research
2. Address content gaps and opportunities
3. Appeal to different audience segments
4. Include unique perspectives not covered by competitors
5. Incorporate specific statistics, case studies, or expert insights
6. Create emotional connection and urgency
7. Provide actionable value to readers
Each angle should be:
- Specific and data-driven
- Unique and differentiated
- Compelling and click-worthy
- Actionable for readers
Respond with JSON:
{{
"content_angles": [
"Specific angle 1 with data/trends",
"Specific angle 2 with unique perspective",
"Specific angle 3 with actionable insights",
"Specific angle 4 with case study focus",
"Specific angle 5 with future outlook",
"Specific angle 6 with problem-solving focus",
"Specific angle 7 with industry insights"
]
}}
"""
from services.llm_providers.gemini_provider import gemini_structured_json_response
angles_schema = {
"type": "object",
"properties": {
"content_angles": {
"type": "array",
"items": {"type": "string"},
"minItems": 5,
"maxItems": 7
}
},
"required": ["content_angles"]
}
angles_result = gemini_structured_json_response(
prompt=angles_prompt,
schema=angles_schema,
temperature=0.7,
max_tokens=800
)
if isinstance(angles_result, dict) and 'content_angles' in angles_result:
return angles_result['content_angles'][:7]
else:
# Fail gracefully - no fallback data
logger.error(f"AI content angles generation failed: {angles_result}")
raise ValueError(f"Content angles generation failed: {angles_result.get('error', 'Unknown error')}")

View File

@@ -0,0 +1,78 @@
"""
Keyword Analyzer - AI-powered keyword analysis for research content.
Extracts and analyzes keywords from research content using structured AI responses.
"""
from typing import Dict, Any, List
from loguru import logger
class KeywordAnalyzer:
"""Analyzes keywords from research content using AI-powered extraction."""
def analyze(self, content: str, original_keywords: List[str]) -> Dict[str, Any]:
"""Parse comprehensive keyword analysis from the research content using AI."""
# Use AI to extract and analyze keywords from the rich research content
keyword_prompt = f"""
Analyze the following research content and extract comprehensive keyword insights for: {', '.join(original_keywords)}
Research Content:
{content[:3000]} # Limit to avoid token limits
Extract and analyze:
1. Primary keywords (main topic terms)
2. Secondary keywords (related terms, synonyms)
3. Long-tail opportunities (specific phrases people search for)
4. Search intent (informational, commercial, navigational, transactional)
5. Keyword difficulty assessment (1-10 scale)
6. Content gaps (what competitors are missing)
7. Semantic keywords (related concepts)
8. Trending terms (emerging keywords)
Respond with JSON:
{{
"primary": ["keyword1", "keyword2"],
"secondary": ["related1", "related2"],
"long_tail": ["specific phrase 1", "specific phrase 2"],
"search_intent": "informational|commercial|navigational|transactional",
"difficulty": 7,
"content_gaps": ["gap1", "gap2"],
"semantic_keywords": ["concept1", "concept2"],
"trending_terms": ["trend1", "trend2"],
"analysis_insights": "Brief analysis of keyword landscape"
}}
"""
from services.llm_providers.gemini_provider import gemini_structured_json_response
keyword_schema = {
"type": "object",
"properties": {
"primary": {"type": "array", "items": {"type": "string"}},
"secondary": {"type": "array", "items": {"type": "string"}},
"long_tail": {"type": "array", "items": {"type": "string"}},
"search_intent": {"type": "string"},
"difficulty": {"type": "integer"},
"content_gaps": {"type": "array", "items": {"type": "string"}},
"semantic_keywords": {"type": "array", "items": {"type": "string"}},
"trending_terms": {"type": "array", "items": {"type": "string"}},
"analysis_insights": {"type": "string"}
},
"required": ["primary", "secondary", "long_tail", "search_intent", "difficulty", "content_gaps", "semantic_keywords", "trending_terms", "analysis_insights"]
}
keyword_analysis = gemini_structured_json_response(
prompt=keyword_prompt,
schema=keyword_schema,
temperature=0.3,
max_tokens=1000
)
if isinstance(keyword_analysis, dict) and 'error' not in keyword_analysis:
return keyword_analysis
else:
# Fail gracefully - no fallback data
logger.error(f"AI keyword analysis failed: {keyword_analysis}")
raise ValueError(f"Keyword analysis failed: {keyword_analysis.get('error', 'Unknown error')}")

View File

@@ -0,0 +1,268 @@
"""
Research Service - Core research functionality for AI Blog Writer.
Handles Google Search grounding, caching, and research orchestration.
"""
from typing import Dict, Any, List
from loguru import logger
from models.blog_models import (
BlogResearchRequest,
BlogResearchResponse,
ResearchSource,
)
from .keyword_analyzer import KeywordAnalyzer
from .competitor_analyzer import CompetitorAnalyzer
from .content_angle_generator import ContentAngleGenerator
class ResearchService:
"""Service for conducting comprehensive research using Google Search grounding."""
def __init__(self):
self.keyword_analyzer = KeywordAnalyzer()
self.competitor_analyzer = CompetitorAnalyzer()
self.content_angle_generator = ContentAngleGenerator()
async def research(self, request: BlogResearchRequest) -> BlogResearchResponse:
"""
Stage 1: Research & Strategy (AI Orchestration)
Uses ONLY Gemini's native Google Search grounding - ONE API call for everything.
Follows LinkedIn service pattern for efficiency and cost optimization.
Includes intelligent caching for exact keyword matches.
"""
try:
from services.llm_providers.gemini_grounded_provider import GeminiGroundedProvider
from services.cache.research_cache import research_cache
topic = request.topic or ", ".join(request.keywords)
industry = request.industry or (request.persona.industry if request.persona and request.persona.industry else "General")
target_audience = getattr(request.persona, 'target_audience', 'General') if request.persona else 'General'
# Check cache first for exact keyword match
cached_result = research_cache.get_cached_result(
keywords=request.keywords,
industry=industry,
target_audience=target_audience
)
if cached_result:
logger.info(f"Returning cached research result for keywords: {request.keywords}")
return BlogResearchResponse(**cached_result)
# Cache miss - proceed with API call
logger.info(f"Cache miss - making API call for keywords: {request.keywords}")
gemini = GeminiGroundedProvider()
# Single comprehensive research prompt - Gemini handles Google Search automatically
research_prompt = f"""
Research the topic "{topic}" in the {industry} industry for {target_audience} audience. Provide a comprehensive analysis including:
1. Current trends and insights (2024-2025)
2. Key statistics and data points with sources
3. Industry expert opinions and quotes
4. Recent developments and news
5. Market analysis and forecasts
6. Best practices and case studies
7. Keyword analysis: primary, secondary, and long-tail opportunities
8. Competitor analysis: top players and content gaps
9. Content angle suggestions: 5 compelling angles for blog posts
Focus on factual, up-to-date information from credible sources.
Include specific data points, percentages, and recent developments.
Structure your response with clear sections for each analysis area.
"""
# Single Gemini call with native Google Search grounding - no fallbacks
gemini_result = await gemini.generate_grounded_content(
prompt=research_prompt,
content_type="research",
max_tokens=2000
)
# Extract sources from grounding metadata
sources = self._extract_sources_from_grounding(gemini_result)
# Extract search widget and queries for UI display
search_widget = gemini_result.get("search_widget", "") or ""
search_queries = gemini_result.get("search_queries", []) or []
# Parse the comprehensive response for different analysis components
content = gemini_result.get("content", "")
keyword_analysis = self.keyword_analyzer.analyze(content, request.keywords)
competitor_analysis = self.competitor_analyzer.analyze(content)
suggested_angles = self.content_angle_generator.generate(content, topic, industry)
logger.info(f"Research completed successfully with {len(sources)} sources and {len(search_queries)} search queries")
# Create the response
response = BlogResearchResponse(
success=True,
sources=sources,
keyword_analysis=keyword_analysis,
competitor_analysis=competitor_analysis,
suggested_angles=suggested_angles,
# Add search widget and queries for UI display
search_widget=search_widget if 'search_widget' in locals() else "",
search_queries=search_queries if 'search_queries' in locals() else [],
)
# Cache the successful result for future exact keyword matches
research_cache.cache_result(
keywords=request.keywords,
industry=industry,
target_audience=target_audience,
result=response.dict()
)
return response
except Exception as e:
error_message = str(e)
logger.error(f"Research failed: {error_message}")
# Return a graceful failure response instead of raising
return BlogResearchResponse(
success=False,
sources=[],
keyword_analysis={},
competitor_analysis={},
suggested_angles=[],
search_widget="",
search_queries=[],
error_message=error_message
)
async def research_with_progress(self, request: BlogResearchRequest, task_id: str) -> BlogResearchResponse:
"""
Research method with progress updates for real-time feedback.
"""
try:
from services.llm_providers.gemini_grounded_provider import GeminiGroundedProvider
from services.cache.research_cache import research_cache
from api.blog_writer.router import _update_progress
topic = request.topic or ", ".join(request.keywords)
industry = request.industry or (request.persona.industry if request.persona and request.persona.industry else "General")
target_audience = getattr(request.persona, 'target_audience', 'General') if request.persona else 'General'
# Check cache first for exact keyword match
await _update_progress(task_id, "🔍 Checking cache for existing research...")
cached_result = research_cache.get_cached_result(
keywords=request.keywords,
industry=industry,
target_audience=target_audience
)
if cached_result:
await _update_progress(task_id, "✅ Found cached research results! Returning instantly...")
logger.info(f"Returning cached research result for keywords: {request.keywords}")
return BlogResearchResponse(**cached_result)
# Cache miss - proceed with API call
await _update_progress(task_id, "🌐 Cache miss - connecting to Google Search grounding...")
logger.info(f"Cache miss - making API call for keywords: {request.keywords}")
gemini = GeminiGroundedProvider()
# Single comprehensive research prompt - Gemini handles Google Search automatically
research_prompt = f"""
Research the topic "{topic}" in the {industry} industry for {target_audience} audience. Provide a comprehensive analysis including:
1. Current trends and insights (2024-2025)
2. Key statistics and data points with sources
3. Industry expert opinions and quotes
4. Recent developments and news
5. Market analysis and forecasts
6. Best practices and case studies
7. Keyword analysis: primary, secondary, and long-tail opportunities
8. Competitor analysis: top players and content gaps
9. Content angle suggestions: 5 compelling angles for blog posts
Focus on factual, up-to-date information from credible sources.
Include specific data points, percentages, and recent developments.
Structure your response with clear sections for each analysis area.
"""
await _update_progress(task_id, "🤖 Making AI request to Gemini with Google Search grounding...")
# Single Gemini call with native Google Search grounding - no fallbacks
gemini_result = await gemini.generate_grounded_content(
prompt=research_prompt,
content_type="research",
max_tokens=2000
)
await _update_progress(task_id, "📊 Processing research results and extracting insights...")
# Extract sources from grounding metadata
sources = self._extract_sources_from_grounding(gemini_result)
# Extract search widget and queries for UI display
search_widget = gemini_result.get("search_widget", "") or ""
search_queries = gemini_result.get("search_queries", []) or []
await _update_progress(task_id, "🔍 Analyzing keywords and content angles...")
# Parse the comprehensive response for different analysis components
content = gemini_result.get("content", "")
keyword_analysis = self.keyword_analyzer.analyze(content, request.keywords)
competitor_analysis = self.competitor_analyzer.analyze(content)
suggested_angles = self.content_angle_generator.generate(content, topic, industry)
await _update_progress(task_id, "💾 Caching results for future use...")
logger.info(f"Research completed successfully with {len(sources)} sources and {len(search_queries)} search queries")
# Create the response
response = BlogResearchResponse(
success=True,
sources=sources,
keyword_analysis=keyword_analysis,
competitor_analysis=competitor_analysis,
suggested_angles=suggested_angles,
# Add search widget and queries for UI display
search_widget=search_widget if 'search_widget' in locals() else "",
search_queries=search_queries if 'search_queries' in locals() else [],
)
# Cache the successful result for future exact keyword matches
research_cache.cache_result(
keywords=request.keywords,
industry=industry,
target_audience=target_audience,
result=response.dict()
)
return response
except Exception as e:
error_message = str(e)
logger.error(f"Research failed: {error_message}")
# Return a graceful failure response instead of raising
return BlogResearchResponse(
success=False,
sources=[],
keyword_analysis={},
competitor_analysis={},
suggested_angles=[],
search_widget="",
search_queries=[],
error_message=error_message
)
def _extract_sources_from_grounding(self, gemini_result: Dict[str, Any]) -> List[ResearchSource]:
"""Extract sources from Gemini grounding metadata."""
sources = []
# The Gemini grounded provider already extracts sources and puts them in the 'sources' field
raw_sources = gemini_result.get("sources", [])
for src in raw_sources:
source = ResearchSource(
title=src.get("title", "Untitled"),
url=src.get("url", ""),
excerpt=src.get("content", "")[:500] if src.get("content") else f"Source from {src.get('title', 'web')}",
credibility_score=float(src.get("credibility_score", 0.8)),
published_at=str(src.get("publication_date", "2024-01-01"))
)
sources.append(source)
return sources

1
backend/services/cache/__init__.py vendored Normal file
View File

@@ -0,0 +1 @@
# Cache services for AI Blog Writer

172
backend/services/cache/research_cache.py vendored Normal file
View File

@@ -0,0 +1,172 @@
"""
Research Cache Service
Provides intelligent caching for Google grounded research results to reduce API costs.
Only returns cached results for exact keyword matches to ensure accuracy.
"""
import hashlib
import json
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
from loguru import logger
class ResearchCache:
"""Cache for research results with exact keyword matching."""
def __init__(self, max_cache_size: int = 100, cache_ttl_hours: int = 24):
"""
Initialize the research cache.
Args:
max_cache_size: Maximum number of cached entries
cache_ttl_hours: Time-to-live for cache entries in hours
"""
self.cache: Dict[str, Dict[str, Any]] = {}
self.max_cache_size = max_cache_size
self.cache_ttl = timedelta(hours=cache_ttl_hours)
def _generate_cache_key(self, keywords: List[str], industry: str, target_audience: str) -> str:
"""
Generate a cache key based on exact keyword match.
Args:
keywords: List of research keywords
industry: Industry context
target_audience: Target audience context
Returns:
MD5 hash of the normalized parameters
"""
# Normalize and sort keywords for consistent hashing
normalized_keywords = sorted([kw.lower().strip() for kw in keywords])
normalized_industry = industry.lower().strip() if industry else "general"
normalized_audience = target_audience.lower().strip() if target_audience else "general"
# Create a consistent string representation
cache_string = f"{normalized_keywords}|{normalized_industry}|{normalized_audience}"
# Generate MD5 hash
return hashlib.md5(cache_string.encode('utf-8')).hexdigest()
def _is_cache_entry_valid(self, entry: Dict[str, Any]) -> bool:
"""Check if a cache entry is still valid (not expired)."""
if 'created_at' not in entry:
return False
created_at = datetime.fromisoformat(entry['created_at'])
return datetime.now() - created_at < self.cache_ttl
def _cleanup_expired_entries(self):
"""Remove expired cache entries."""
expired_keys = []
for key, entry in self.cache.items():
if not self._is_cache_entry_valid(entry):
expired_keys.append(key)
for key in expired_keys:
del self.cache[key]
logger.debug(f"Removed expired cache entry: {key}")
def _evict_oldest_entries(self, num_to_evict: int):
"""Evict the oldest cache entries when cache is full."""
# Sort by creation time and remove oldest entries
sorted_entries = sorted(
self.cache.items(),
key=lambda x: x[1].get('created_at', ''),
reverse=False
)
for i in range(min(num_to_evict, len(sorted_entries))):
key = sorted_entries[i][0]
del self.cache[key]
logger.debug(f"Evicted oldest cache entry: {key}")
def get_cached_result(self, keywords: List[str], industry: str, target_audience: str) -> Optional[Dict[str, Any]]:
"""
Get cached research result for exact keyword match.
Args:
keywords: List of research keywords
industry: Industry context
target_audience: Target audience context
Returns:
Cached research result if found and valid, None otherwise
"""
cache_key = self._generate_cache_key(keywords, industry, target_audience)
if cache_key not in self.cache:
logger.debug(f"Cache miss for keywords: {keywords}")
return None
entry = self.cache[cache_key]
# Check if entry is still valid
if not self._is_cache_entry_valid(entry):
del self.cache[cache_key]
logger.debug(f"Cache entry expired for keywords: {keywords}")
return None
logger.info(f"Cache hit for keywords: {keywords} (saved API call)")
return entry.get('result')
def cache_result(self, keywords: List[str], industry: str, target_audience: str, result: Dict[str, Any]):
"""
Cache a research result.
Args:
keywords: List of research keywords
industry: Industry context
target_audience: Target audience context
result: Research result to cache
"""
cache_key = self._generate_cache_key(keywords, industry, target_audience)
# Cleanup expired entries first
self._cleanup_expired_entries()
# Check if cache is full and evict if necessary
if len(self.cache) >= self.max_cache_size:
num_to_evict = len(self.cache) - self.max_cache_size + 1
self._evict_oldest_entries(num_to_evict)
# Store the result
self.cache[cache_key] = {
'result': result,
'created_at': datetime.now().isoformat(),
'keywords': keywords,
'industry': industry,
'target_audience': target_audience
}
logger.info(f"Cached research result for keywords: {keywords}")
def get_cache_stats(self) -> Dict[str, Any]:
"""Get cache statistics."""
self._cleanup_expired_entries()
return {
'total_entries': len(self.cache),
'max_size': self.max_cache_size,
'ttl_hours': self.cache_ttl.total_seconds() / 3600,
'entries': [
{
'keywords': entry['keywords'],
'industry': entry['industry'],
'target_audience': entry['target_audience'],
'created_at': entry['created_at']
}
for entry in self.cache.values()
]
}
def clear_cache(self):
"""Clear all cached entries."""
self.cache.clear()
logger.info("Research cache cleared")
# Global cache instance
research_cache = ResearchCache()

View File

@@ -9,6 +9,8 @@ Based on Google AI's official grounding documentation.
import os
import json
import re
import time
import asyncio
from typing import List, Dict, Any, Optional
from datetime import datetime
from loguru import logger
@@ -104,6 +106,29 @@ class GeminiGroundedProvider:
)
except asyncio.TimeoutError:
raise Exception(f"Gemini API request timed out after {self.timeout} seconds")
except Exception as api_error:
# Handle specific Google API errors with retry logic
error_str = str(api_error)
if "503" in error_str and "overloaded" in error_str:
# Conservative retry for overloaded service (expensive API calls)
response = await self._retry_with_backoff(
lambda: self._make_api_request(grounded_prompt, config),
max_retries=1, # Only 1 retry to avoid excessive costs
base_delay=5 # Longer delay
)
elif "429" in error_str:
# Conservative retry for rate limits
response = await self._retry_with_backoff(
lambda: self._make_api_request(grounded_prompt, config),
max_retries=1, # Only 1 retry
base_delay=10 # Much longer delay for rate limits
)
elif "401" in error_str or "403" in error_str:
raise Exception("Authentication failed. Please check your API credentials.")
elif "400" in error_str:
raise Exception("Invalid request. Please check your input parameters.")
else:
raise Exception(f"Google AI service error: {error_str}")
# Process the grounded response
result = self._process_grounded_response(response, content_type)
@@ -112,9 +137,47 @@ class GeminiGroundedProvider:
return result
except Exception as e:
logger.error(f"❌ Error generating grounded content: {str(e)}")
# Log error without causing secondary exceptions
try:
logger.error(f"❌ Error generating grounded content: {str(e)}")
except:
# Fallback to print if logging fails
print(f"Error generating grounded content: {str(e)}")
raise
async def _make_api_request(self, grounded_prompt: str, config: Any):
"""Make the actual API request to Gemini."""
import concurrent.futures
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor() as executor:
return await asyncio.wait_for(
loop.run_in_executor(
executor,
lambda: self.client.models.generate_content(
model="gemini-2.5-flash",
contents=grounded_prompt,
config=config,
)
),
timeout=self.timeout
)
async def _retry_with_backoff(self, func, max_retries: int = 3, base_delay: float = 1.0):
"""Retry a function with exponential backoff."""
for attempt in range(max_retries + 1):
try:
return await func()
except Exception as e:
if attempt == max_retries:
# Last attempt failed, raise the error
raise e
# Calculate delay with exponential backoff
delay = base_delay * (2 ** attempt)
logger.warning(f"Attempt {attempt + 1} failed, retrying in {delay} seconds: {str(e)}")
await asyncio.sleep(delay)
def _build_grounded_prompt(self, prompt: str, content_type: str) -> str:
"""
Build a prompt optimized for grounded content generation.

View File

@@ -389,11 +389,37 @@ def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9,
config=generation_config,
)
# According to the documentation, we should use response.parsed for structured output
# Check for parsed content first (primary method for structured output)
if hasattr(response, 'parsed') and response.parsed is not None:
logger.info("Using response.parsed for structured output")
return response.parsed
# Check for text content as fallback
if hasattr(response, 'text') and response.text:
logger.info("No parsed content, trying to parse text response")
try:
import json
parsed_text = json.loads(response.text)
logger.info("Successfully parsed text as JSON")
return parsed_text
except json.JSONDecodeError as e:
logger.error(f"Failed to parse text as JSON: {e}")
# Check candidates for content (fallback for edge cases)
if hasattr(response, 'candidates') and response.candidates:
candidate = response.candidates[0]
if hasattr(candidate, 'content') and candidate.content:
if hasattr(candidate.content, 'parts') and candidate.content.parts:
for part in candidate.content.parts:
if hasattr(part, 'text') and part.text:
try:
import json
parsed_text = json.loads(part.text)
logger.info("Successfully parsed candidate text as JSON")
return parsed_text
except json.JSONDecodeError as e:
logger.error(f"Failed to parse candidate text as JSON: {e}")
logger.error("No valid structured response content found")
return {"error": "No valid structured response content found"}

View File

@@ -5,6 +5,25 @@
- **Approach**: Copilot-first UX using CopilotKit. Reuse LinkedIn assistive writing patterns: Google Search grounding, Exa research, hallucination detector, quality analysis, citations.
- **User Interaction Model**: The user only talks to the Copilot; the editor reflects all state and changes via generative UI and HITL confirmations.
### 🚀 **Current Implementation Status** (Updated: December 2024)
**✅ COMPLETED PHASES:**
- **Stage 1: Research & Strategy** - ✅ FULLY IMPLEMENTED
- **Stage 2: Content Planning (Outline)** - ✅ FULLY IMPLEMENTED
- **Backend Architecture** - ✅ MODULAR & PRODUCTION-READY
- **Frontend UI Components** - ✅ COMPREHENSIVE EDITOR
- **CopilotKit Integration** - ✅ FULLY FUNCTIONAL
**🔄 IN PROGRESS:**
- **Stage 3: Content Generation** - 🔄 PARTIALLY IMPLEMENTED
- **Stage 4: SEO & Publishing** - 🔄 PARTIALLY IMPLEMENTED
**📋 TODO:**
- Section-by-section content generation
- Full SEO optimization pipeline
- Publishing integrations (Wix/WordPress)
- Advanced quality checks
### Key Principles
- **AI-first, HITL**: The assistant leads with intelligent suggestions; the user approves via render-and-wait HITL components where appropriate.
- **Research fidelity**: Google grounding + Exa researcher; hallucination detection with claim verification; pervasive citations.
@@ -16,78 +35,125 @@
## 1) Workflow (4 Stages)
### Stage 1: Research & Strategy (AI Orchestration)
Inputs
- `keywords: string[]`, `industry: string`, `targetAudience: string`, `tone: string`, `wordCountTarget: number`, `userId`
- Persona is fetched from DB and persisted in session
### Stage 1: Research & Strategy (AI Orchestration) ✅ **FULLY IMPLEMENTED**
Backend/Services
- Reuse LinkedIn research handler patterns: Google native grounding (Gemini provider), optional Exa research.
- Reuse hallucination detector service and models: `/api/hallucination-detector/*` for claim extraction and verification.
**✅ IMPLEMENTED FEATURES:**
- **Google Search Grounding**: Single Gemini API call with native Google Search integration
- **Intelligent Caching**: Exact keyword match caching to reduce API costs
- **AI-Powered Analysis**: Keyword analysis, competitor analysis, content angle generation
- **Robust Error Handling**: No fallback data - only real AI-generated insights or graceful failures
- **Progress Tracking**: Real-time progress messages during research operations
CopilotKit Actions
- `getPersonaFromDB(userId)` → persona constraints and style.
- `analyzeKeywords(keywords, industry, audience)` → search intent, primary/secondary/long-tail, difficulty, volume.
- `researchTopic(topic, depth, sources=['google','exa'])` → aggregated research sources (with credibility + timestamps).
- `analyzeCompetitors(keywords, industry)` → top pages, headings used, gaps/opportunities.
**✅ IMPLEMENTED INPUTS:**
- `keywords: string[]`, `industry: string`, `targetAudience: string`, `wordCountTarget: number`
- Persona support (basic implementation)
Generative UI (render-only)
- Research Summary card: sources, credibility score, proposed angles.
- Suggested Keywords: chip list; add/remove HITL.
**✅ IMPLEMENTED BACKEND/SERVICES:**
- **Modular Architecture**: `ResearchService`, `KeywordAnalyzer`, `CompetitorAnalyzer`, `ContentAngleGenerator`
- **Google Grounding**: Native Gemini Google Search integration (no Exa dependency)
- **Caching System**: Intelligent research result caching with TTL and LRU eviction
- **Error Handling**: Graceful failure with specific error messages
Suggestions (programmatic)
- “Confirm research”, “Refine keywords, “Add competitor”, “Proceed to outline”.
**✅ IMPLEMENTED COPILOTKIT ACTIONS:**
- `researchTopic(keywords, industry, target_audience, blogLength)` → comprehensive research with sources
- `chatWithResearchData(question)` → interactive research data exploration
- `getResearchKeywords()` → HITL keyword collection form
- `performResearch(formData)` → research execution with form data
**✅ IMPLEMENTED GENERATIVE UI:**
- **ResearchResults Component**: Sources, credibility scores, keyword analysis, content angles
- **KeywordInputForm**: HITL form for keyword collection with blog length selection
- **Progress Messages**: Real-time loading states with CopilotKit status system
**✅ IMPLEMENTED SUGGESTIONS:**
- "I want to research a topic for my blog" (initial)
- "Let's proceed to create an Outline" (post-research)
- "Chat with Research Data" (exploration)
- "Create outline with custom inputs" (advanced)
---
### Stage 2: Content Planning (AI + Human)
Deliverables
- Structured outline (H1/H2/H3), per-section key points, citations to use, target word counts.
### Stage 2: Content Planning (AI + Human) ✅ **FULLY IMPLEMENTED**
CopilotKit Actions
- `generateOutline(research, persona, wordCount)` → full outline with per-section targets and suggested refs.
- `refineOutline(operation, sectionId, payload?)` → add/remove/move/merge sections (HITL diff in UI).
- `attachReferences(sectionId, sourceIds[])` → associate sources to sections.
**✅ IMPLEMENTED DELIVERABLES:**
- **Structured Outline**: H1/H2/H3 hierarchy with per-section key points and target word counts
- **AI-Generated Titles**: Multiple title options with SEO optimization
- **Research Integration**: Outline sections linked to research sources and keywords
- **Word Count Distribution**: Intelligent word allocation across sections
Generative UI (HITL)
- Outline Editor: draggable sections/subsections, per-section references and target words, persona style hints.
**✅ IMPLEMENTED COPILOTKIT ACTIONS:**
- `generateOutline()` → AI-powered outline generation from research data
- `createOutlineWithCustomInputs(customInstructions)` → custom outline with user instructions
- `refineOutline(operation, sectionId, payload)` → add/remove/move/merge/rename sections
- `enhanceSection(sectionId, focus)` → AI enhancement of individual sections
- `optimizeOutline(focus)` → AI optimization of entire outline
- `rebalanceOutline(targetWords)` → word count rebalancing across sections
Suggestions
- “Generate [Section 1]”, “Regenerate [Section 2]”, “Attach sources to [Section]”, “Generate All Sections”.
**✅ IMPLEMENTED GENERATIVE UI:**
- **EnhancedOutlineEditor**: Interactive outline editor with expandable sections
- **TitleSelector**: AI-generated title options with custom title creation
- **CustomOutlineForm**: HITL form for custom outline instructions
- **Section Management**: Add, edit, reorder, merge sections with visual feedback
- **Research Integration**: Source references and keyword suggestions per section
**✅ IMPLEMENTED SUGGESTIONS:**
- "Generate outline" (standard)
- "Create outline with custom inputs" (advanced)
- "Enhance section [X]" (section-specific)
- "Optimize entire outline" (global)
- "Rebalance word counts" (distribution)
---
### Stage 3: Content Generation (CopilotKit-only, no multi-agent)
Deliverables
- Long-form markdown content with inline citations, persona-aligned tone, and sectioned structure.
### Stage 3: Content Generation (CopilotKit-only, no multi-agent) 🔄 **PARTIALLY IMPLEMENTED**
CopilotKit Actions
- `generateSection(sectionPlan, keywords, tone, persona, refs[])` → returns markdown + inline cites.
- `generateAllSections(outline)` → sequential section generation with progress render.
- `optimizeSection(content, goals[])` → readability/EEAT/examples/data improvements; UI shows diff preview (HITL confirm).
- `runHallucinationCheck(content)` → uses `/api/hallucination-detector/detect` to flag claims + propose fixes.
**🔄 PARTIALLY IMPLEMENTED DELIVERABLES:**
- **Section Generation**: Basic section generation with markdown output
- **Content Structure**: Sectioned markdown with inline citations support
- **Quality Checks**: Hallucination detection integration
Editor/UI Updates
- Per-section markdown tabs; word count; inline citation chips; section mini-SEO score.
- DiffPreview component for any AI edit prior to apply.
**✅ IMPLEMENTED COPILOTKIT ACTIONS:**
- `generateSection(sectionId)` → generates content for specific section
- `generateAllSections()` → placeholder for bulk generation
- `runHallucinationCheck()` → integrates with hallucination detector service
Suggestions
- “Add table/figure”, “Insert case study with source”, “Strengthen introduction”, “Tighten conclusion CTA”.
**🔄 PARTIALLY IMPLEMENTED UI:**
- **Section Editors**: Basic markdown editing per section
- **DiffPreview Component**: Exists but needs integration
- **Citation System**: Basic structure in place
**📋 TODO:**
- Full section-by-section content generation
- Advanced content optimization
- Inline citation management
- Content quality improvements
- Progress tracking for bulk generation
---
### Stage 4: Optimization & Publishing (AI + Human)
SEO Optimization
- `analyzeSEO(content, keywords)` → density, heading structure, links, readability, image alt coverage, overall SEO score.
- `generateSEOMetadata(content, title, keywords)` → title options, meta description, OG/Twitter cards, schema Article/FAQ.
- `applySEOFixes(suggestions[])` → diff preview + HITL apply.
### Stage 4: Optimization & Publishing (AI + Human) 🔄 **PARTIALLY IMPLEMENTED**
Publishing
- `prepareForPublish(platform: 'wix' | 'wordpress')` → HTML + images + metadata packaging.
- `publishToPlatform(platform, schedule?)` → uses Wix/WordPress clients (ToBeMigrated integrations). Shows URL/status.
**🔄 PARTIALLY IMPLEMENTED SEO OPTIMIZATION:**
- **SEO Analysis**: Basic SEO analysis with keyword density and structure
- **Metadata Generation**: Title options and meta description generation
- **SEO Integration**: Wraps existing SEO tools services
Suggestions
- “Run SEO analysis”, “Apply recommended fixes”, “Generate metadata”, “Publish to WordPress”, “Schedule on Wix”.
**✅ IMPLEMENTED COPILOTKIT ACTIONS:**
- `runSEOAnalyze(keywords)` → SEO analysis with scores and recommendations
- `generateSEOMetadata(title)` → metadata generation for titles and descriptions
- `publishToPlatform(platform, schedule)` → placeholder for publishing
**🔄 PARTIALLY IMPLEMENTED UI:**
- **SEOMiniPanel**: Basic SEO analysis display
- **Metadata Management**: Title and description editing
**📋 TODO:**
- Full SEO optimization pipeline
- Advanced SEO recommendations
- Publishing integrations (Wix/WordPress)
- Content optimization with diff preview
- Image alt text and media management
- Schema markup generation
---
@@ -136,49 +202,84 @@ Persistence
---
## 4) Backend APIs
## 4) Backend APIs ✅ **FULLY IMPLEMENTED**
New Blog Endpoints
- `POST /api/blog/research`inputs: keywords/industry/audience/tone/wordCount, personaId?; returns research bundle.
- `POST /api/blog/outline/generate` → returns structured outline with targets and ref suggestions.
- `POST /api/blog/outline/refine` → returns updated outline (operation-based).
- `POST /api/blog/section/generate`returns markdown + inline citations.
- `POST /api/blog/section/optimize` → returns optimized content + rationale.
- `POST /api/blog/quality/hallucination-check` → proxies hallucination detector results for blog.
- `POST /api/blog/seo/analyze` → wraps SEO analyzers; returns scores/suggestions.
- `POST /api/blog/seo/metadata` → returns title/meta/OG/Twitter/schema.
- `POST /api/blog/publish` → platform: wix|wordpress, schedule?; returns URL/status.
**✅ IMPLEMENTED BLOG ENDPOINTS:**
- `POST /api/blog/research`comprehensive research with Google Search grounding
- `POST /api/blog/research/start` → async research with progress tracking
- `GET /api/blog/research/status/{task_id}` → research progress status
- `POST /api/blog/outline/generate`AI-powered outline generation
- `POST /api/blog/outline/start` → async outline generation with progress
- `GET /api/blog/outline/status/{task_id}` → outline progress status
- `POST /api/blog/outline/refine` → outline refinement operations
- `POST /api/blog/outline/rebalance` → word count rebalancing
- `POST /api/blog/section/generate` → section content generation
- `POST /api/blog/section/optimize` → content optimization
- `POST /api/blog/quality/hallucination-check` → hallucination detection
- `POST /api/blog/seo/analyze` → SEO analysis and recommendations
- `POST /api/blog/seo/metadata` → metadata generation
- `POST /api/blog/publish` → publishing to platforms
- `GET /api/blog/health` → service health check
Reuse
- `/api/hallucination-detector/detect|extract-claims|verify-claim|health` (already implemented).
**✅ IMPLEMENTED MODULAR ARCHITECTURE:**
- **Core Service**: `BlogWriterService` - main orchestrator
- **Research Module**: `ResearchService`, `KeywordAnalyzer`, `CompetitorAnalyzer`, `ContentAngleGenerator`
- **Outline Module**: `OutlineService`, `OutlineGenerator`, `OutlineOptimizer`, `SectionEnhancer`
- **Caching System**: Intelligent research result caching with TTL and LRU eviction
- **Error Handling**: Graceful failure with specific error messages
Models (indicative)
**✅ IMPLEMENTED MODELS:**
- `BlogResearchRequest`, `BlogResearchResponse`
- `BlogOutline`, `BlogOutlineRefinement`
- `BlogOutlineRequest`, `BlogOutlineResponse`, `BlogOutlineRefineRequest`
- `BlogSectionRequest`, `BlogSectionResponse`
- `BlogSEOAnalysisRequest`, `BlogSEOMetadataResponse`
- `BlogOptimizeRequest`, `BlogOptimizeResponse`
- `BlogSEOAnalyzeRequest`, `BlogSEOAnalyzeResponse`
- `BlogSEOMetadataRequest`, `BlogSEOMetadataResponse`
- `BlogPublishRequest`, `BlogPublishResponse`
- `HallucinationCheckRequest`, `HallucinationCheckResponse`
**✅ REUSED SERVICES:**
- `/api/hallucination-detector/*` - hallucination detection integration
- SEO tools services - wrapped for blog-specific analysis
---
## 5) CopilotKit Action Inventory
## 5) CopilotKit Action Inventory ✅ **COMPREHENSIVE IMPLEMENTATION**
Research
- `getPersonaFromDB`, `analyzeKeywords`, `researchTopic`, `analyzeCompetitors`
**✅ RESEARCH ACTIONS (FULLY IMPLEMENTED):**
- `researchTopic(keywords, industry, target_audience, blogLength)` → comprehensive research
- `chatWithResearchData(question)` → interactive research exploration
- `getResearchKeywords()` → HITL keyword collection form
- `performResearch(formData)` → research execution with form data
Planning
- `generateOutline`, `refineOutline`, `attachReferences`
**✅ PLANNING ACTIONS (FULLY IMPLEMENTED):**
- `generateOutline()` → AI-powered outline generation
- `createOutlineWithCustomInputs(customInstructions)` → custom outline creation
- `refineOutline(operation, sectionId, payload)` → outline refinement operations
- `enhanceSection(sectionId, focus)` → section enhancement
- `optimizeOutline(focus)` → outline optimization
- `rebalanceOutline(targetWords)` → word count rebalancing
Generation
- `generateSection`, `generateAllSections`, `optimizeSection`, `runHallucinationCheck`
**🔄 GENERATION ACTIONS (PARTIALLY IMPLEMENTED):**
- `generateSection(sectionId)` → section content generation ✅
- `generateAllSections()` → bulk generation (placeholder) 🔄
- `runHallucinationCheck()` → hallucination detection ✅
SEO
- `analyzeSEO`, `generateSEOMetadata`, `applySEOFixes`
**🔄 SEO ACTIONS (PARTIALLY IMPLEMENTED):**
- `runSEOAnalyze(keywords)` → SEO analysis ✅
- `generateSEOMetadata(title)` → metadata generation ✅
Publishing
- `prepareForPublish`, `publishToPlatform`
**🔄 PUBLISHING ACTIONS (PARTIALLY IMPLEMENTED):**
- `publishToPlatform(platform, schedule)` → publishing (placeholder) 🔄
UX/Render-only/HITL
- `showResearchCard`, `showOutlineEditor`, `showDiffPreview`, `showSEOPanel`, `showPublishDialog`
**✅ UX/RENDER-ONLY/HITL (FULLY IMPLEMENTED):**
- `ResearchResults` → research data visualization
- `EnhancedOutlineEditor` → interactive outline management
- `KeywordInputForm` → HITL keyword collection
- `CustomOutlineForm` → HITL custom outline creation
- `TitleSelector` → title selection and creation
- `DiffPreview` → content diff visualization
- `SEOMiniPanel` → SEO analysis display
---
@@ -201,26 +302,117 @@ Final
---
## 7) Delivery Plan / Milestones
## 7) Delivery Plan / Milestones ✅ **UPDATED STATUS**
Milestone 1: Research + Outline
- Actions: persona load, analyze keywords, research topic, generate outline, outline editor (HITL)
**✅ MILESTONE 1: Research + Outline (COMPLETED)**
- Actions: research topic, generate outline, outline editor (HITL)
- ✅ Google Search grounding integration
- ✅ AI-powered keyword and competitor analysis
- ✅ Interactive outline editor with refinement capabilities
- ✅ Research data visualization and exploration
Milestone 2: Section Generation + Quality
- generateSection/generateAllSections, optimizeSection with diff preview, hallucination check + fixes
**🔄 MILESTONE 2: Section Generation + Quality (IN PROGRESS)**
- generateSection (basic implementation)
- 🔄 generateAllSections (needs full implementation)
- 🔄 optimizeSection with diff preview (needs integration)
- ✅ hallucination check integration
- 📋 Content quality improvements and optimization
Milestone 3: SEO & Metadata
- analyzeSEO panel, generateSEOMetadata (title/meta/OG/Twitter/schema), apply fixes
**🔄 MILESTONE 3: SEO & Metadata (IN PROGRESS)**
- analyzeSEO panel (basic implementation)
- ✅ generateSEOMetadata (title/meta generation)
- 📋 Advanced SEO recommendations and fixes
- 📋 Schema markup and social media optimization
Milestone 4: Publishing
- prepareForPublish, publishToPlatform (Wix/WordPress), schedule, success URL
**📋 MILESTONE 4: Publishing (TODO)**
- 📋 prepareForPublish functionality
- 📋 publishToPlatform (Wix/WordPress integration)
- 📋 Scheduling and publishing workflow
- 📋 Success URL and status tracking
Milestone 5: Polish
- Readability aids, version history, performance, accessibility
**📋 MILESTONE 5: Polish (TODO)**
- 📋 Advanced readability aids
- 📋 Version history and auto-save
- 📋 Performance optimization
- 📋 Accessibility improvements
---
## 8) References
## 8) Current Architecture & Implementation Details
### 🏗️ **Backend Architecture (Modular & Production-Ready)**
**Core Service Structure:**
```
backend/services/blog_writer/
├── core/
│ └── blog_writer_service.py # Main orchestrator
├── research/
│ ├── research_service.py # Research orchestration
│ ├── keyword_analyzer.py # AI keyword analysis
│ ├── competitor_analyzer.py # Competitor intelligence
│ └── content_angle_generator.py # Content angle discovery
├── outline/
│ ├── outline_service.py # Outline orchestration
│ ├── outline_generator.py # AI outline generation
│ ├── outline_optimizer.py # Outline optimization
│ └── section_enhancer.py # Section enhancement
└── blog_service.py # Entry point (thin wrapper)
```
**Key Features:**
- **No Fallback Data**: Only real AI-generated insights or graceful failures
- **Intelligent Caching**: Research result caching with TTL and LRU eviction
- **Error Handling**: Specific error messages and retry logic
- **Progress Tracking**: Real-time progress updates for long-running operations
### 🎨 **Frontend Architecture (CopilotKit-First)**
**Component Structure:**
```
frontend/src/components/BlogWriter/
├── BlogWriter.tsx # Main orchestrator component
├── ResearchAction.tsx # Research CopilotKit actions
├── ResearchResults.tsx # Research data visualization
├── KeywordInputForm.tsx # HITL keyword collection
├── EnhancedOutlineEditor.tsx # Interactive outline editor
├── TitleSelector.tsx # Title selection and creation
├── CustomOutlineForm.tsx # HITL custom outline creation
├── ResearchDataActions.tsx # Research data interaction
├── EnhancedOutlineActions.tsx # Outline management actions
├── DiffPreview.tsx # Content diff visualization
└── SEOMiniPanel.tsx # SEO analysis display
```
**Key Features:**
- **CopilotKit Integration**: Full action system with HITL components
- **Real-time Updates**: Progress messages and status tracking
- **Interactive UI**: Drag-and-drop, expandable sections, visual feedback
- **Error Handling**: User-friendly error messages and recovery
### 🔧 **Technical Implementation Highlights**
**Research Phase:**
- Single Gemini API call with Google Search grounding
- AI-powered analysis of keywords, competitors, and content angles
- Intelligent caching to reduce API costs
- No fallback data - only real AI insights
**Outline Phase:**
- Research-driven outline generation
- Interactive outline editor with full CRUD operations
- AI-powered section enhancement and optimization
- Word count rebalancing and distribution
**Quality Assurance:**
- Robust error handling with specific messages
- Progress tracking for long-running operations
- Graceful failure without misleading data
- Real-time user feedback and guidance
---
## 9) References
- CopilotKit Quickstart, Frontend Actions, Generative UI, HITL, Suggestions
- Quickstart: https://docs.copilotkit.ai/direct-to-llm/guides/quickstart
- Frontend Actions: https://docs.copilotkit.ai/frontend-actions

View File

@@ -10,6 +10,9 @@ import SEOMiniPanel from './SEOMiniPanel';
import ResearchResults from './ResearchResults';
import KeywordInputForm from './KeywordInputForm';
import ResearchAction from './ResearchAction';
import { CustomOutlineForm } from './CustomOutlineForm';
import { ResearchDataActions } from './ResearchDataActions';
import { EnhancedOutlineActions } from './EnhancedOutlineActions';
const useCopilotActionTyped = useCopilotAction as any;
@@ -141,9 +144,22 @@ export const BlogWriter: React.FC = () => {
} catch (error) {
console.error('Outline generation failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Provide more specific error messages based on the error type
let userMessage = '❌ Outline generation failed. ';
if (errorMessage.includes('503') || errorMessage.includes('overloaded')) {
userMessage += 'The AI service is temporarily overloaded. Please try again in a few minutes.';
} else if (errorMessage.includes('timeout')) {
userMessage += 'The request timed out. Please try again.';
} else if (errorMessage.includes('Invalid outline structure')) {
userMessage += 'The AI generated an invalid response. Please try again with different research data.';
} else {
userMessage += `${errorMessage}. Please try again or contact support if the problem persists.`;
}
return {
success: false,
message: `❌ Outline generation failed: ${errorMessage}. The AI system encountered an issue while creating your outline. Please try again or contact support if the problem persists.`
message: userMessage
};
}
return {
@@ -411,6 +427,9 @@ export const BlogWriter: React.FC = () => {
}
});
// Publish (convert markdown -> HTML rudimentary; TODO: replace with proper converter like marked)
useCopilotActionTyped({
name: 'publishToPlatform',
@@ -439,17 +458,36 @@ export const BlogWriter: React.FC = () => {
const suggestions = useMemo(() => {
const items = [] as { title: string; message: string }[];
if (!research) items.push({ title: '🔎 Start research', message: "I want to research a topic for my blog" });
if (research && outline.length === 0) items.push({ title: '🧩 Create Outline', message: 'Let\'s proceed to create an outline based on the research results' });
if (outline.length > 0) {
if (!research) {
items.push({ title: '🔎 Start research', message: "I want to research a topic for my blog" });
} else if (research && outline.length === 0) {
// Research completed, guide user to outline creation
items.push({
title: '🧩 Create Outline',
message: 'Let\'s proceed to create an outline based on the research results'
});
items.push({
title: '💬 Chat with Research Data',
message: 'I want to explore the research data and ask questions about the findings'
});
items.push({
title: '🎨 Create Custom Outline',
message: 'I want to create an outline with my own specific instructions and requirements'
});
} else if (outline.length > 0) {
// Outline created, focus on content generation
items.push({ title: '📝 Generate all sections', message: 'Generate all sections of my blog post' });
outline.forEach(s => items.push({ title: `✍️ Generate ${s.heading}`, message: `Generate the section: ${s.heading}` }));
items.push({ title: '🔧 Refine outline', message: 'Help me refine the outline structure' });
items.push({ title: '✨ Enhance outline', message: 'Optimize the entire outline for better flow and engagement' });
items.push({ title: '⚖️ Rebalance word counts', message: 'Rebalance word count distribution across sections' });
items.push({ title: '📈 Run SEO analysis', message: 'Analyze SEO for my blog post' });
items.push({ title: '🧾 Generate SEO metadata', message: 'Generate SEO metadata and title' });
items.push({ title: '🧪 Hallucination check', message: 'Check for any false claims in my content' });
items.push({ title: '🚀 Publish to WordPress', message: 'Publish my blog to WordPress' });
}
return items;
}, [research, outline]);
@@ -457,7 +495,17 @@ export const BlogWriter: React.FC = () => {
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Extracted Components */}
<KeywordInputForm onResearchComplete={handleResearchComplete} />
<CustomOutlineForm onOutlineCreated={setOutline} />
<ResearchAction onResearchComplete={handleResearchComplete} />
<ResearchDataActions
research={research}
onOutlineCreated={setOutline}
onTitleOptionsSet={setTitleOptions}
/>
<EnhancedOutlineActions
outline={outline}
onOutlineUpdated={setOutline}
/>
<div style={{ padding: 16, borderBottom: '1px solid #eee' }}>
<h2 style={{ margin: 0 }}>AI Blog Writer</h2>
@@ -551,25 +599,40 @@ Available tools:
- getResearchKeywords(prompt?: string) - Get keywords from user for research
- performResearch(formData: string) - Perform research with collected keywords (formData is JSON string with keywords and blogLength)
- researchTopic(keywords: string, industry?: string, target_audience?: string)
- chatWithResearchData(question: string) - Chat with research data to explore insights and get recommendations
- generateOutline()
- createOutlineWithCustomInputs(customInstructions: string) - Create outline with user's custom instructions
- generateSection(sectionId: string)
- generateAllSections()
- refineOutline(operation: add|remove|move|merge|rename, sectionId?: string, payload?: object)
- enhanceSection(sectionId: string, focus?: string) - Enhance a specific section with AI improvements
- optimizeOutline(focus?: string) - Optimize entire outline for better flow, SEO, and engagement
- rebalanceOutline(targetWords?: number) - Rebalance word count distribution across sections
- runSEOAnalyze(keywords?: string)
- generateSEOMetadata(title?: string)
- runHallucinationCheck()
- publishToPlatform(platform: 'wix'|'wordpress', schedule_time?: string)
CRITICAL BEHAVIOR:
CRITICAL BEHAVIOR & USER GUIDANCE:
- When user wants to research ANY topic, IMMEDIATELY call getResearchKeywords() to get their input
- When user asks to research something, call getResearchKeywords() first to collect their keywords
- After getResearchKeywords() completes, IMMEDIATELY call performResearch() with the collected data
- When user asks for outline, call generateOutline()
USER GUIDANCE STRATEGY:
- After research completion, ALWAYS guide user toward outline creation as the next step
- If user wants to explore research data, use chatWithResearchData() but then guide them to outline creation
- If user has specific outline requirements, use createOutlineWithCustomInputs() with their instructions
- When user asks for outline, call generateOutline() or createOutlineWithCustomInputs() based on their needs
- When user asks to generate content, call generateSection or generateAllSections
ENGAGEMENT TACTICS:
- DO NOT ask for clarification - take action immediately with the information provided
- Always call the appropriate tool instead of just talking about what you could do
- Be aware of the current state and reference research results when relevant
- Guide users through the process: Research → Outline → Content → SEO → Publish
- Use encouraging language and highlight progress made
- If user seems lost, remind them of the current stage and suggest the next step
- When research is complete, emphasize the value of the data found and guide to outline creation
`;
return [toolGuide, additional].filter(Boolean).join('\n\n');
}}

View File

@@ -0,0 +1,142 @@
import React, { useState } from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
const useCopilotActionTyped = useCopilotAction as any;
interface CustomOutlineFormProps {
onOutlineCreated?: (outline: any) => void;
}
export const CustomOutlineForm: React.FC<CustomOutlineFormProps> = ({ onOutlineCreated }) => {
const [customInstructions, setCustomInstructions] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
useCopilotActionTyped({
name: 'getCustomOutlineInstructions',
description: 'Get custom instructions from user for outline generation',
parameters: [
{ name: 'prompt', type: 'string', description: 'Prompt to show user', required: false }
],
renderAndWaitForResponse: ({ respond, args, status }: { respond?: (value: string) => void; args: { prompt?: string }; status: string }) => {
if (status === 'complete') {
return (
<div style={{
padding: '16px',
backgroundColor: '#f0f8ff',
borderRadius: '8px',
border: '1px solid #1976d2'
}}>
<p style={{ margin: 0, color: '#1976d2', fontWeight: '500' }}>
Custom outline instructions received! Creating your personalized outline...
</p>
</div>
);
}
return (
<div style={{
padding: '20px',
backgroundColor: '#f8f9fa',
borderRadius: '12px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}>
<h4 style={{ margin: '0 0 16px 0', color: '#333' }}>
🎨 Create Custom Outline
</h4>
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px' }}>
{args.prompt || 'Tell me your specific requirements for the blog outline. What should it focus on? What structure do you prefer?'}
</p>
<div style={{ display: 'grid', gap: '12px' }}>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Custom Instructions *
</label>
<textarea
value={customInstructions}
onChange={(e) => setCustomInstructions(e.target.value)}
placeholder="e.g., Focus on beginner-friendly explanations, include case studies, emphasize practical applications, create a step-by-step guide format..."
style={{
width: '100%',
minHeight: '120px',
padding: '12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box',
resize: 'vertical',
fontFamily: 'inherit'
}}
autoFocus
autoComplete="off"
spellCheck="true"
/>
</div>
<div style={{
padding: '12px',
backgroundColor: '#e3f2fd',
borderRadius: '6px',
border: '1px solid #1976d2'
}}>
<h5 style={{ margin: '0 0 8px 0', color: '#1976d2', fontSize: '14px' }}>💡 Examples:</h5>
<ul style={{ margin: '0', paddingLeft: '20px', fontSize: '13px', color: '#333' }}>
<li>"Focus on beginner-friendly explanations with practical examples"</li>
<li>"Include case studies and real-world applications"</li>
<li>"Create a step-by-step tutorial format"</li>
<li>"Emphasize the business benefits and ROI"</li>
<li>"Make it more technical and detailed for developers"</li>
</ul>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '16px' }}>
<button
onClick={() => {
if (customInstructions.trim()) {
respond?.(customInstructions.trim());
} else {
window.alert('Please provide your custom instructions for the outline.');
}
}}
disabled={!customInstructions.trim() || isSubmitting}
style={{
backgroundColor: customInstructions.trim() ? '#1976d2' : '#ccc',
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '10px 20px',
cursor: customInstructions.trim() ? 'pointer' : 'not-allowed',
fontSize: '14px',
fontWeight: '500',
flex: 1
}}
>
{isSubmitting ? '⏳ Creating...' : '🚀 Create Custom Outline'}
</button>
<button
onClick={() => respond?.('CANCEL')}
style={{
backgroundColor: 'transparent',
border: '1px solid #ddd',
borderRadius: '6px',
padding: '10px 20px',
cursor: 'pointer',
fontSize: '14px',
color: '#666'
}}
>
Cancel
</button>
</div>
</div>
);
}
});
return null; // This component only provides the CopilotKit action, no UI
};

View File

@@ -0,0 +1,200 @@
import React from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
import { blogWriterApi, BlogOutlineSection } from '../../services/blogWriterApi';
const useCopilotActionTyped = useCopilotAction as any;
interface EnhancedOutlineActionsProps {
outline: BlogOutlineSection[];
onOutlineUpdated: (outline: BlogOutlineSection[]) => void;
}
export const EnhancedOutlineActions: React.FC<EnhancedOutlineActionsProps> = ({
outline,
onOutlineUpdated
}) => {
// Enhanced Outline Actions
useCopilotActionTyped({
name: 'enhanceSection',
description: 'Enhance a specific outline section with AI improvements',
parameters: [
{ name: 'sectionId', type: 'string', description: 'ID of the section to enhance', required: true },
{ name: 'focus', type: 'string', description: 'Enhancement focus (SEO, engagement, depth, etc.)', required: false }
],
handler: async ({ sectionId, focus = 'general improvement' }: { sectionId: string; focus?: string }) => {
const section = outline.find(s => s.id === sectionId);
if (!section) return { success: false, message: 'Section not found' };
try {
const enhancedSection = await blogWriterApi.enhanceSection(section, focus);
onOutlineUpdated(outline.map(s => s.id === sectionId ? enhancedSection : s));
return {
success: true,
message: `Enhanced section "${section.heading}" with focus on ${focus}`,
enhanced_section: enhancedSection
};
} catch (error) {
return { success: false, message: `Enhancement failed: ${error}` };
}
},
render: ({ status }: any) => {
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{
padding: '16px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid #9c27b0',
borderTop: '2px solid transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<h4 style={{ margin: 0, color: '#9c27b0' }}> Enhancing Section</h4>
</div>
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.5' }}>
<p style={{ margin: '0 0 8px 0' }}> Analyzing section content and structure...</p>
<p style={{ margin: '0 0 8px 0' }}> Generating enhanced subheadings and key points...</p>
<p style={{ margin: '0' }}> Optimizing for better engagement and SEO...</p>
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
return null;
}
});
useCopilotActionTyped({
name: 'optimizeOutline',
description: 'Optimize entire outline for better flow, SEO, and engagement',
parameters: [
{ name: 'focus', type: 'string', description: 'Optimization focus (flow, SEO, engagement, etc.)', required: false }
],
handler: async ({ focus = 'general optimization' }: { focus?: string }) => {
if (outline.length === 0) return { success: false, message: 'No outline to optimize' };
try {
const optimizedOutline = await blogWriterApi.optimizeOutline({ outline }, focus);
onOutlineUpdated(optimizedOutline.outline);
return {
success: true,
message: `Optimized outline with focus on ${focus}`,
optimized_outline: optimizedOutline.outline
};
} catch (error) {
return { success: false, message: `Optimization failed: ${error}` };
}
},
render: ({ status }: any) => {
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{
padding: '16px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid #ff9800',
borderTop: '2px solid transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<h4 style={{ margin: 0, color: '#ff9800' }}>🎯 Optimizing Outline</h4>
</div>
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.5' }}>
<p style={{ margin: '0 0 8px 0' }}> Analyzing outline structure and flow...</p>
<p style={{ margin: '0 0 8px 0' }}> Optimizing headings for SEO and engagement...</p>
<p style={{ margin: '0' }}> Improving narrative progression and reader experience...</p>
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
return null;
}
});
useCopilotActionTyped({
name: 'rebalanceOutline',
description: 'Rebalance word count distribution across outline sections',
parameters: [
{ name: 'targetWords', type: 'number', description: 'Target total word count', required: false }
],
handler: async ({ targetWords = 1500 }: { targetWords?: number }) => {
if (outline.length === 0) return { success: false, message: 'No outline to rebalance' };
try {
const rebalancedOutline = await blogWriterApi.rebalanceOutline({ outline }, targetWords);
onOutlineUpdated(rebalancedOutline.outline);
return {
success: true,
message: `Rebalanced outline for ${targetWords} words`,
rebalanced_outline: rebalancedOutline.outline
};
} catch (error) {
return { success: false, message: `Rebalancing failed: ${error}` };
}
},
render: ({ status }: any) => {
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{
padding: '16px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid #4caf50',
borderTop: '2px solid transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<h4 style={{ margin: 0, color: '#4caf50' }}> Rebalancing Word Counts</h4>
</div>
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.5' }}>
<p style={{ margin: '0 0 8px 0' }}> Calculating optimal word distribution...</p>
<p style={{ margin: '0 0 8px 0' }}> Adjusting section word counts...</p>
<p style={{ margin: '0' }}> Ensuring balanced content structure...</p>
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
return null;
}
});
return null; // This component only provides the CopilotKit actions, no UI
};

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState } from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
import { blogWriterApi, BlogResearchRequest, BlogResearchResponse } from '../../services/blogWriterApi';
@@ -9,23 +9,136 @@ interface KeywordInputFormProps {
onResearchComplete?: (researchData: BlogResearchResponse) => void;
}
export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsReceived, onResearchComplete }) => {
// State for button enable/disable only
const [hasInput, setHasInput] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const selectRef = useRef<HTMLSelectElement>(null);
// Separate component to manage form state
const ResearchForm: React.FC<{
prompt?: string;
onSubmit: (data: { keywords: string; blogLength: string }) => void;
onCancel: () => void;
}> = ({ prompt, onSubmit, onCancel }) => {
const [keywords, setKeywords] = useState('');
const [blogLength, setBlogLength] = useState('1000');
const hasValidInput = keywords.trim().length > 0;
// Focus input when form appears
useEffect(() => {
if (inputRef.current) {
setTimeout(() => {
inputRef.current?.focus();
inputRef.current?.select();
}, 100);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (hasValidInput) {
onSubmit({ keywords: keywords.trim(), blogLength });
} else {
window.alert('Please enter keywords or a topic to start research.');
}
}, []);
};
return (
<form
onSubmit={handleSubmit}
style={{
padding: '20px',
backgroundColor: '#f8f9fa',
borderRadius: '12px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}
>
<h4 style={{ margin: '0 0 16px 0', color: '#333' }}>
🔍 Let's Research Your Blog Topic
</h4>
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px' }}>
{prompt || 'Please provide the keywords or topic you want to research for your blog:'}
</p>
<div style={{ display: 'grid', gap: '12px' }}>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Keywords or Topic *
</label>
<input
type="text"
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
onFocus={(e) => e.target.select()}
placeholder="e.g., artificial intelligence, machine learning, AI trends"
style={{
width: '100%',
padding: '10px 12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box'
}}
autoFocus
autoComplete="off"
spellCheck="false"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Blog Length (words)
</label>
<select
value={blogLength}
onChange={(e) => setBlogLength(e.target.value)}
style={{
width: '100%',
padding: '10px 12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box'
}}
>
<option value="500">500 words (Short blog)</option>
<option value="1000">1000 words (Medium blog)</option>
<option value="1500">1500 words (Long blog)</option>
<option value="2000">2000+ words (Comprehensive guide)</option>
</select>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '16px' }}>
<button
type="submit"
disabled={!hasValidInput}
style={{
backgroundColor: hasValidInput ? '#1976d2' : '#ccc',
color: 'white',
border: 'none',
borderRadius: '6px',
padding: '10px 20px',
cursor: hasValidInput ? 'pointer' : 'not-allowed',
fontSize: '14px',
fontWeight: '500',
flex: 1
}}
>
🚀 Start Research {hasValidInput ? '(Enabled)' : '(Disabled)'}
</button>
<button
type="button"
onClick={onCancel}
style={{
backgroundColor: 'transparent',
border: '1px solid #ddd',
borderRadius: '6px',
padding: '10px 20px',
cursor: 'pointer',
fontSize: '14px',
color: '#666'
}}
>
Cancel
</button>
</div>
</form>
);
};
export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsReceived, onResearchComplete }) => {
// Keyword input action with Human-in-the-Loop
useCopilotActionTyped({
@@ -51,143 +164,14 @@ export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsRe
}
return (
<form
key="keyword-input-form"
onSubmit={(e) => {
e.preventDefault();
<ResearchForm
prompt={args.prompt}
onSubmit={(formData) => {
onKeywordsReceived?.(formData);
respond?.(JSON.stringify(formData));
}}
style={{
padding: '20px',
backgroundColor: '#f8f9fa',
borderRadius: '12px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}
>
<h4 style={{ margin: '0 0 16px 0', color: '#333' }}>
🔍 Let's Research Your Blog Topic
</h4>
<p style={{ margin: '0 0 16px 0', color: '#666', fontSize: '14px' }}>
{args.prompt || 'Please provide the keywords or topic you want to research for your blog:'}
</p>
<div style={{ display: 'grid', gap: '12px' }}>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Keywords or Topic *
</label>
<input
ref={inputRef}
type="text"
defaultValue=""
onChange={(e) => {
const value = e.target.value;
// Update state for button enable/disable
setHasInput(value.trim().length > 0);
}}
onFocus={(e) => {
e.target.select();
}}
placeholder="e.g., artificial intelligence, machine learning, AI trends"
style={{
width: '100%',
padding: '10px 12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box'
}}
autoFocus
autoComplete="off"
spellCheck="false"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', fontWeight: '500' }}>
Blog Length (words)
</label>
<select
ref={selectRef}
defaultValue="1000"
onChange={(e) => {
// No state update needed for select
}}
style={{
width: '100%',
padding: '10px 12px',
border: '2px solid #1976d2',
borderRadius: '6px',
fontSize: '14px',
outline: 'none',
backgroundColor: 'white',
boxSizing: 'border-box'
}}
>
<option value="500">500 words (Short blog)</option>
<option value="1000">1000 words (Medium blog)</option>
<option value="1500">1500 words (Long blog)</option>
<option value="2000">2000+ words (Comprehensive guide)</option>
</select>
</div>
</div>
<div style={{ display: 'flex', gap: '12px', marginTop: '16px' }}>
<button
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
const kw = (inputRef.current?.value || '').trim();
const len = (selectRef.current?.value || '1000');
if (kw) {
const formData = {
keywords: kw,
blogLength: len
};
// Notify parent component if callback provided
onKeywordsReceived?.(formData);
// Send to CopilotKit to trigger performResearch action
respond?.(JSON.stringify(formData));
}
}}
disabled={!hasInput}
style={{
backgroundColor: hasInput ? '#1976d2' : '#f5f5f5',
color: hasInput ? 'white' : '#999',
border: 'none',
borderRadius: '6px',
padding: '10px 20px',
cursor: hasInput ? 'pointer' : 'not-allowed',
fontSize: '14px',
fontWeight: '500',
flex: 1
}}
>
🚀 Start Research
</button>
<button
onClick={() => {
respond?.('CANCEL');
}}
style={{
backgroundColor: 'transparent',
border: '1px solid #ddd',
borderRadius: '6px',
padding: '10px 20px',
cursor: 'pointer',
fontSize: '14px',
color: '#666'
}}
>
Cancel
</button>
</div>
</form>
onCancel={() => respond?.('CANCEL')}
/>
);
}
});
@@ -204,7 +188,6 @@ export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsRe
const data = JSON.parse(formData);
const { keywords, blogLength } = data;
// If keywords is a topic description, extract keywords from it
const keywordList = keywords.includes(',')
? keywords.split(',').map((k: string) => k.trim())
: keywords.split(' ').filter((k: string) => k.length > 2).slice(0, 5);
@@ -217,8 +200,6 @@ export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsRe
};
const res = await blogWriterApi.research(payload);
// Notify parent component
onResearchComplete?.(res);
const sourcesCount = res.sources?.length || 0;
@@ -246,7 +227,6 @@ export const KeywordInputForm: React.FC<KeywordInputFormProps> = ({ onKeywordsRe
}
},
render: ({ status }: any) => {
console.log('performResearch render called with status:', status);
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{

View File

@@ -23,7 +23,7 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
// If keywords is a topic description, extract keywords from it
const keywordList = keywords.includes(',')
? keywords.split(',').map(k => k.trim())
: keywords.split(' ').filter(k => k.length > 2).slice(0, 5); // Extract up to 5 meaningful words
: keywords.split(' ').filter(k => k.length > 1).slice(0, 5); // Extract up to 5 meaningful words (including 2-char words like "AI")
const payload: BlogResearchRequest = {
keywords: keywordList,
@@ -34,6 +34,15 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
const res = await blogWriterApi.research(payload);
// Check if research failed gracefully
if (!res.success) {
return {
success: false,
message: `❌ Research failed: ${res.error_message || 'Unknown error occurred'}. Please try again with different keywords or contact support if the problem persists.`,
error_details: res.error_message
};
}
// Notify parent component
onResearchComplete?.(res);
@@ -63,7 +72,7 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
}
},
render: ({ status }: any) => {
if (status === 'inProgress') {
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{
padding: '16px',
@@ -84,10 +93,12 @@ export const ResearchAction: React.FC<ResearchActionProps> = ({ onResearchComple
<h4 style={{ margin: 0, color: '#1976d2' }}>🔍 Researching Your Topic</h4>
</div>
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.5' }}>
<p style={{ margin: '0 0 8px 0' }}> Starting research operation...</p>
<p style={{ margin: '0 0 8px 0' }}> Connecting to Google Search grounding...</p>
<p style={{ margin: '0 0 8px 0' }}> Analyzing keywords and search intent...</p>
<p style={{ margin: '0 0 8px 0' }}> Gathering relevant sources and statistics...</p>
<p style={{ margin: '0' }}> Generating content angles and search queries...</p>
<p style={{ margin: '0 0 8px 0' }}> Generating content angles and search queries...</p>
<p style={{ margin: '0', fontStyle: 'italic', color: '#888' }}> This may take 1-3 minutes. Please wait...</p>
</div>
<style>{`
@keyframes spin {

View File

@@ -0,0 +1,179 @@
import React from 'react';
import { useCopilotAction } from '@copilotkit/react-core';
import { blogWriterApi, BlogResearchResponse, BlogOutlineSection } from '../../services/blogWriterApi';
const useCopilotActionTyped = useCopilotAction as any;
interface ResearchDataActionsProps {
research: BlogResearchResponse | null;
onOutlineCreated: (outline: BlogOutlineSection[]) => void;
onTitleOptionsSet: (titles: string[]) => void;
}
export const ResearchDataActions: React.FC<ResearchDataActionsProps> = ({
research,
onOutlineCreated,
onTitleOptionsSet
}) => {
// Chat with Research Data
useCopilotActionTyped({
name: 'chatWithResearchData',
description: 'Chat with the research data to explore insights, ask questions, and get recommendations',
parameters: [
{ name: 'question', type: 'string', description: 'Question or topic to explore in the research data', required: true }
],
handler: async ({ question }: { question: string }) => {
if (!research) {
return {
success: false,
message: 'No research data available. Please complete research first.',
suggestion: 'Try asking: "I want to research a topic for my blog"'
};
}
// Provide comprehensive research context for the copilot to answer intelligently
const researchContext = {
sources: research.sources.length,
primaryKeywords: research.keyword_analysis?.primary || [],
secondaryKeywords: research.keyword_analysis?.secondary || [],
longTailKeywords: research.keyword_analysis?.long_tail || [],
searchIntent: research.keyword_analysis?.search_intent || 'informational',
contentAngles: research.suggested_angles || [],
competitorAnalysis: research.competitor_analysis || {},
searchQueries: research.search_queries || [],
topSources: research.sources.slice(0, 5).map(s => ({
title: s.title,
credibility: s.credibility_score,
excerpt: s.excerpt?.substring(0, 200)
}))
};
return {
success: true,
message: `I can help you explore the research data! Here's what I found:`,
research_context: researchContext,
user_question: question,
next_step_suggestion: 'Ready to create an outline? Try: "Create outline with custom inputs" or "Let\'s proceed to create an outline"'
};
},
render: ({ status, result }: any) => {
if (status === 'complete' && result?.success) {
return (
<div style={{
padding: '16px',
backgroundColor: '#f0f8ff',
borderRadius: '8px',
border: '1px solid #1976d2',
margin: '8px 0'
}}>
<h4 style={{ margin: '0 0 12px 0', color: '#1976d2' }}>🔍 Research Data Insights</h4>
<div style={{ fontSize: '14px', color: '#333', lineHeight: '1.5', marginBottom: '12px' }}>
<p style={{ margin: '0 0 8px 0' }}><strong>Your Question:</strong> {result.user_question}</p>
<p style={{ margin: '0 0 8px 0' }}><strong>Research Summary:</strong></p>
<ul style={{ margin: '0 0 8px 0', paddingLeft: '20px' }}>
<li>{result.research_context.sources} authoritative sources found</li>
<li>Primary keywords: {result.research_context.primaryKeywords.join(', ')}</li>
<li>Search intent: {result.research_context.searchIntent}</li>
<li>{result.research_context.contentAngles.length} content angles identified</li>
</ul>
</div>
<div style={{
padding: '12px',
backgroundColor: '#e3f2fd',
borderRadius: '6px',
border: '1px solid #1976d2'
}}>
<p style={{ margin: '0 0 8px 0', fontWeight: '500', color: '#1976d2' }}>💡 Next Step:</p>
<p style={{ margin: '0', fontSize: '14px', color: '#333' }}>{result.next_step_suggestion}</p>
</div>
</div>
);
}
return null;
}
});
// Create Outline with Custom Inputs
useCopilotActionTyped({
name: 'createOutlineWithCustomInputs',
description: 'Create an outline with custom instructions and requirements from the user',
parameters: [
{ name: 'customInstructions', type: 'string', description: 'Custom instructions for outline generation', required: true }
],
handler: async ({ customInstructions }: { customInstructions: string }) => {
if (!research) {
return {
success: false,
message: 'No research data available. Please complete research first.',
suggestion: 'Try asking: "I want to research a topic for my blog"'
};
}
try {
// Create a custom outline request with user instructions
const customOutlineRequest = {
research: research,
word_count: 1500,
custom_instructions: customInstructions
};
const outlineResponse = await blogWriterApi.generateOutline(customOutlineRequest);
onOutlineCreated(outlineResponse.outline);
onTitleOptionsSet(outlineResponse.title_options);
return {
success: true,
message: `Created custom outline with ${outlineResponse.outline.length} sections based on your instructions: "${customInstructions}"`,
outline_sections: outlineResponse.outline.length,
title_options: outlineResponse.title_options.length,
next_step_suggestion: 'Great! Now you can enhance sections, generate content, or refine the outline further.'
};
} catch (error) {
return {
success: false,
message: `Custom outline creation failed: ${error}`,
suggestion: 'Try providing more specific instructions or ask me to help refine your requirements.'
};
}
},
render: ({ status }: any) => {
if (status === 'inProgress' || status === 'executing') {
return (
<div style={{
padding: '16px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e0e0e0',
margin: '8px 0'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
<div style={{
width: '20px',
height: '20px',
border: '2px solid #673ab7',
borderTop: '2px solid transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<h4 style={{ margin: 0, color: '#673ab7' }}>🎨 Creating Custom Outline</h4>
</div>
<div style={{ fontSize: '14px', color: '#666', lineHeight: '1.5' }}>
<p style={{ margin: '0 0 8px 0' }}> Analyzing your custom instructions...</p>
<p style={{ margin: '0 0 8px 0' }}> Applying requirements to research data...</p>
<p style={{ margin: '0' }}> Generating tailored outline structure...</p>
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
return null;
}
});
return null; // This component only provides the CopilotKit actions, no UI
};

View File

@@ -1,4 +1,4 @@
import { apiClient } from "../api/client";
import { apiClient, aiApiClient, longRunningApiClient } from "../api/client";
export interface PersonaInfo {
persona_id?: string;
@@ -33,6 +33,7 @@ export interface BlogResearchResponse {
suggested_angles: string[];
search_widget?: string;
search_queries?: string[];
error_message?: string;
}
export interface BlogOutlineSection {
@@ -86,11 +87,13 @@ export interface BlogPublishResponse {
export const blogWriterApi = {
async research(payload: BlogResearchRequest): Promise<BlogResearchResponse> {
// Use the direct research endpoint for simplicity
const { data } = await apiClient.post("/api/blog/research", payload);
return data;
},
async generateOutline(payload: { research: BlogResearchResponse; persona?: PersonaInfo; word_count?: number }): Promise<BlogOutlineResponse> {
async generateOutline(payload: { research: BlogResearchResponse; persona?: PersonaInfo; word_count?: number; custom_instructions?: string }): Promise<BlogOutlineResponse> {
// Use the direct outline generation endpoint
const { data } = await apiClient.post("/api/blog/outline/generate", payload);
return data;
},
@@ -118,6 +121,28 @@ export const blogWriterApi = {
async publish(payload: { platform: 'wix' | 'wordpress'; html: string; metadata: BlogSEOMetadataResponse; schedule_time?: string }): Promise<BlogPublishResponse> {
const { data } = await apiClient.post("/api/blog/publish", payload);
return data;
},
// Enhanced Outline Methods
async enhanceSection(section: BlogOutlineSection, focus: string = 'general improvement'): Promise<BlogOutlineSection> {
const { data } = await apiClient.post("/api/blog/outline/enhance-section", section, {
params: { focus }
});
return data;
},
async optimizeOutline(payload: { outline: BlogOutlineSection[] }, focus: string = 'general optimization'): Promise<{ outline: BlogOutlineSection[] }> {
const { data } = await apiClient.post("/api/blog/outline/optimize", payload, {
params: { focus }
});
return data;
},
async rebalanceOutline(payload: { outline: BlogOutlineSection[] }, targetWords: number = 1500): Promise<{ outline: BlogOutlineSection[] }> {
const { data } = await apiClient.post("/api/blog/outline/rebalance", payload, {
params: { target_words: targetWords }
});
return data;
}
};