Alpha Subscription Implementation Plan
This commit is contained in:
@@ -1,268 +0,0 @@
|
||||
# Google Search Console (GSC) Integration for ALwrity
|
||||
|
||||
This document describes the complete Google Search Console integration implemented for ALwrity, allowing users to connect their GSC accounts and fetch real website analytics data.
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
### Backend Features
|
||||
- **OAuth2 Authentication**: Secure Google OAuth2 flow for GSC access
|
||||
- **User Credential Management**: Encrypted storage of user OAuth tokens
|
||||
- **Data Caching**: SQLite-based caching system for GSC data
|
||||
- **Multi-user Support**: Each user can connect their own GSC account
|
||||
- **Real-time Analytics**: Fetch live search analytics, sitemaps, and site data
|
||||
- **Comprehensive Logging**: Detailed logging throughout the system
|
||||
|
||||
### Frontend Features
|
||||
- **GSC Login Button**: Seamless OAuth connection flow
|
||||
- **Status Management**: Real-time connection status display
|
||||
- **Popup Authentication**: Secure OAuth flow in popup window
|
||||
- **Error Handling**: Comprehensive error management and user feedback
|
||||
- **Responsive UI**: Material-UI components matching existing dashboard style
|
||||
|
||||
## 📁 File Structure
|
||||
|
||||
### Backend Files
|
||||
```
|
||||
backend/
|
||||
├── services/
|
||||
│ └── gsc_service.py # Core GSC service with OAuth and data management
|
||||
├── routers/
|
||||
│ └── gsc_auth.py # FastAPI router for GSC endpoints
|
||||
├── middleware/
|
||||
│ └── auth_middleware.py # Clerk authentication middleware
|
||||
├── gsc_credentials.json # Google OAuth2 client credentials
|
||||
├── env_template.txt # Environment variables template
|
||||
└── requirements.txt # Updated with GSC dependencies
|
||||
```
|
||||
|
||||
### Frontend Files
|
||||
```
|
||||
frontend/src/
|
||||
├── api/
|
||||
│ └── gsc.ts # GSC API client
|
||||
├── components/SEODashboard/components/
|
||||
│ ├── GSCLoginButton.tsx # GSC connection UI component
|
||||
│ └── GSCAuthCallback.tsx # OAuth callback handler
|
||||
├── env_template.txt # Frontend environment template
|
||||
└── package.json # Updated with Clerk dependencies
|
||||
```
|
||||
|
||||
## 🔧 API Endpoints
|
||||
|
||||
### GSC Authentication & Management
|
||||
- `GET /gsc/auth/url` - Get OAuth authorization URL
|
||||
- `GET /gsc/callback` - Handle OAuth callback
|
||||
- `GET /gsc/status` - Check GSC connection status
|
||||
- `DELETE /gsc/disconnect` - Revoke GSC access
|
||||
|
||||
### GSC Data Retrieval
|
||||
- `GET /gsc/sites` - Get user's GSC sites
|
||||
- `POST /gsc/analytics` - Fetch search analytics data
|
||||
- `GET /gsc/sitemaps/{site_url}` - Get sitemaps for a site
|
||||
- `GET /gsc/health` - Health check endpoint
|
||||
|
||||
## 🗄️ Database Schema
|
||||
|
||||
### GSC Credentials Table
|
||||
```sql
|
||||
CREATE TABLE gsc_credentials (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
credentials_json TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### GSC Data Cache Table
|
||||
```sql
|
||||
CREATE TABLE gsc_data_cache (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
site_url TEXT NOT NULL,
|
||||
data_type TEXT NOT NULL,
|
||||
data_json TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES gsc_credentials (user_id)
|
||||
);
|
||||
```
|
||||
|
||||
## 🔐 Authentication Flow
|
||||
|
||||
1. **User clicks "Connect GSC"** → Frontend requests OAuth URL
|
||||
2. **Backend generates OAuth URL** → Returns Google authorization URL
|
||||
3. **User authorizes in popup** → Google redirects to callback
|
||||
4. **Backend handles callback** → Exchanges code for tokens
|
||||
5. **Credentials stored securely** → User can now access GSC data
|
||||
6. **Real data replaces mock data** → Dashboard shows live analytics
|
||||
|
||||
## 🛠️ Setup Instructions
|
||||
|
||||
### 1. Backend Setup
|
||||
|
||||
1. **Install Dependencies**:
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **Configure Environment**:
|
||||
```bash
|
||||
cp env_template.txt .env
|
||||
# Edit .env with your actual values
|
||||
```
|
||||
|
||||
3. **Google OAuth Setup**:
|
||||
- Copy your Google OAuth credentials to `gsc_credentials.json`
|
||||
- Ensure redirect URIs include both backend and frontend URLs
|
||||
|
||||
4. **Start Backend**:
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
### 2. Frontend Setup
|
||||
|
||||
1. **Install Dependencies**:
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Configure Environment**:
|
||||
```bash
|
||||
cp env_template.txt .env
|
||||
# Edit .env with your actual values
|
||||
```
|
||||
|
||||
3. **Start Frontend**:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## 🔑 Environment Variables
|
||||
|
||||
### Backend (.env)
|
||||
```env
|
||||
# Clerk Authentication
|
||||
CLERK_SECRET_KEY=your_clerk_secret_key_here
|
||||
|
||||
# Google Search Console
|
||||
GSC_REDIRECT_URI=http://localhost:8000/gsc/callback
|
||||
|
||||
# Development Settings
|
||||
DISABLE_AUTH=false
|
||||
```
|
||||
|
||||
### Frontend (.env)
|
||||
```env
|
||||
# Clerk Authentication
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key_here
|
||||
|
||||
# CopilotKit
|
||||
REACT_APP_COPILOTKIT_API_KEY=your_copilotkit_api_key_here
|
||||
```
|
||||
|
||||
## 📊 Data Types Retrieved
|
||||
|
||||
### Search Analytics
|
||||
- **Clicks**: Number of clicks from search results
|
||||
- **Impressions**: Number of times site appeared in search
|
||||
- **CTR**: Click-through rate percentage
|
||||
- **Position**: Average position in search results
|
||||
|
||||
### Site Information
|
||||
- **Site URLs**: List of verified sites in GSC
|
||||
- **Permission Levels**: User's access level for each site
|
||||
|
||||
### Sitemaps
|
||||
- **Sitemap Paths**: URLs of submitted sitemaps
|
||||
- **Submission Dates**: When sitemaps were last submitted
|
||||
- **Index Status**: Which pages are indexed
|
||||
|
||||
## 🔒 Security Features
|
||||
|
||||
- **OAuth2 Security**: Google's secure authorization protocol
|
||||
- **Token Encryption**: Credentials stored securely in database
|
||||
- **User Isolation**: Each user's data is completely separate
|
||||
- **Token Refresh**: Automatic token refresh when expired
|
||||
- **Access Revocation**: Users can disconnect at any time
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Backend Testing
|
||||
```bash
|
||||
cd backend
|
||||
python -m pytest test_gsc_*.py
|
||||
```
|
||||
|
||||
### Frontend Testing
|
||||
```bash
|
||||
cd frontend
|
||||
npm test
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
1. Start both backend and frontend servers
|
||||
2. Navigate to SEO Dashboard
|
||||
3. Click "Connect GSC"
|
||||
4. Complete OAuth flow
|
||||
5. Verify real data appears in dashboard
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"Not Found" Error**:
|
||||
- Check API endpoint paths match between frontend and backend
|
||||
- Ensure backend server is running
|
||||
|
||||
2. **"Not Authenticated" Error**:
|
||||
- Verify Clerk API keys are correct
|
||||
- Check environment variables are loaded
|
||||
|
||||
3. **OAuth Popup Blocked**:
|
||||
- Allow popups for localhost
|
||||
- Check browser popup settings
|
||||
|
||||
4. **GSC Data Not Loading**:
|
||||
- Verify Google OAuth credentials
|
||||
- Check user has verified sites in GSC
|
||||
- Review backend logs for errors
|
||||
|
||||
## 📈 Performance Optimizations
|
||||
|
||||
- **Data Caching**: GSC data cached for 1 hour to reduce API calls
|
||||
- **Lazy Loading**: Components load data only when needed
|
||||
- **Error Boundaries**: Graceful error handling prevents crashes
|
||||
- **Connection Pooling**: Efficient database connections
|
||||
|
||||
## 🔄 Future Enhancements
|
||||
|
||||
- **Real-time Updates**: WebSocket-based live data updates
|
||||
- **Advanced Analytics**: More detailed GSC metrics and insights
|
||||
- **Bulk Operations**: Analyze multiple sites simultaneously
|
||||
- **Export Features**: Export GSC data to CSV/Excel
|
||||
- **Scheduled Reports**: Automated GSC reports via email
|
||||
|
||||
## 📝 Logging
|
||||
|
||||
The system includes comprehensive logging at all levels:
|
||||
|
||||
- **Backend**: Detailed logs for OAuth flow, data retrieval, and errors
|
||||
- **Frontend**: Console logs for API calls and user interactions
|
||||
- **Database**: Query logging for debugging data issues
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
When contributing to the GSC integration:
|
||||
|
||||
1. Follow existing code patterns and style
|
||||
2. Add comprehensive logging for new features
|
||||
3. Include error handling for all API calls
|
||||
4. Update tests for any new functionality
|
||||
5. Document any new environment variables or setup steps
|
||||
|
||||
## 📄 License
|
||||
|
||||
This GSC integration is part of the ALwrity project and follows the same licensing terms.
|
||||
@@ -1,639 +0,0 @@
|
||||
import os
|
||||
import streamlit as st
|
||||
from loguru import logger
|
||||
|
||||
from lib.utils.voice_processing import record_voice
|
||||
from lib.ai_writers.ai_blog_writer.blog_writer_styles import apply_blog_writer_styles
|
||||
from lib.ai_writers.ai_blog_writer.ai_blog_generator_utils import (
|
||||
CONFIG_PATH,
|
||||
load_config,
|
||||
get_search_params_from_config,
|
||||
get_blog_characteristics_from_config,
|
||||
get_blog_images_from_config,
|
||||
get_llm_options_from_config,
|
||||
process_input,
|
||||
handle_content_generation
|
||||
)
|
||||
|
||||
apply_blog_writer_styles()
|
||||
|
||||
def display_input_section():
|
||||
"""Display the input section with text area, file upload, and voice recording options."""
|
||||
# Main container with columns for better organization
|
||||
col1, col2, col3 = st.columns([2, 1.5, 0.5])
|
||||
|
||||
# First column: Keywords input
|
||||
with col1:
|
||||
st.markdown("### 📌 Content Source")
|
||||
user_input = st.text_area(
|
||||
'Power your content with keywords or a website URL',
|
||||
help='Provide keywords, a blog title, YouTube link, or web URL to generate targeted content.',
|
||||
placeholder="Examples:\n- Keywords: AI tools, digital marketing\n- Blog Title: The Future of AI in Marketing\n- YouTube Link: https://youtube.com/...\n- Web URL: https://example.com/...",
|
||||
height=150
|
||||
)
|
||||
|
||||
# Second column: File uploader
|
||||
with col2:
|
||||
st.markdown("### 📁 File Upload")
|
||||
uploaded_file = st.file_uploader(
|
||||
"Add files to enhance your content",
|
||||
type=["txt", "pdf", "docx", "jpg", "jpeg", "png", "mp3", "wav", "mp4", "mkv", "avi"],
|
||||
help='Upload documents, images, or media files to incorporate additional information in your blog.'
|
||||
)
|
||||
|
||||
# Third column: Voice input
|
||||
with col3:
|
||||
st.markdown("### 🎤 Voice")
|
||||
audio_input = record_voice()
|
||||
if audio_input:
|
||||
st.success("Voice recorded!")
|
||||
|
||||
return user_input, uploaded_file, audio_input
|
||||
|
||||
|
||||
def display_content_type_selection(inside_expander=False):
|
||||
"""Display the content type selection section and return the selected type.
|
||||
|
||||
Args:
|
||||
inside_expander (bool): If True, adjust heading levels for display inside an expander.
|
||||
"""
|
||||
# Content options in a cleaner layout
|
||||
if not inside_expander:
|
||||
st.markdown("### 🔧 Content Configuration")
|
||||
st.markdown("#### Select Content Type")
|
||||
else:
|
||||
st.markdown("#### Content Type")
|
||||
|
||||
# Content type selection with better UI
|
||||
content_type = st.radio(
|
||||
"Choose the format and length of your blog content",
|
||||
["Standard Blog Post", "Comprehensive Long-form", "AI Agent Team (Beta)"],
|
||||
horizontal=True,
|
||||
help="Standard: 800-1200 words | Long-form: 1500+ words | AI Agent: Experimental multi-perspective content"
|
||||
)
|
||||
|
||||
# Map the friendly content type names to the original options
|
||||
content_type_map = {
|
||||
"Standard Blog Post": "Normal-length content",
|
||||
"Comprehensive Long-form": "Long-form content",
|
||||
"AI Agent Team (Beta)": "Experimental - AI Agents team"
|
||||
}
|
||||
|
||||
return content_type, content_type_map[content_type]
|
||||
|
||||
|
||||
def display_content_characteristics_tab():
|
||||
"""Display the Content Characteristics tab and return the selected options."""
|
||||
st.markdown("#### Blog Content Characteristics")
|
||||
|
||||
# Load default values from configuration
|
||||
config_blog_chars = get_blog_characteristics_from_config()
|
||||
|
||||
# Blog length
|
||||
blog_length = st.number_input(
|
||||
"Blog Length (words)",
|
||||
min_value=500,
|
||||
max_value=5000,
|
||||
value=int(config_blog_chars.get("blog_length", 2000)),
|
||||
step=100,
|
||||
help="Target word count for your blog post"
|
||||
)
|
||||
|
||||
# Blog tone
|
||||
tone_options = ["Professional", "Casual", "Formal", "Conversational", "Authoritative", "Friendly"]
|
||||
default_tone = config_blog_chars.get("blog_tone", "Professional")
|
||||
default_tone_index = tone_options.index(default_tone) if default_tone in tone_options else 0
|
||||
|
||||
blog_tone = st.selectbox(
|
||||
"Blog Tone",
|
||||
options=tone_options,
|
||||
index=default_tone_index,
|
||||
help="The overall tone and style of your blog content"
|
||||
)
|
||||
|
||||
# Blog demographic
|
||||
demographic_options = ["Professional", "General", "Technical", "Beginner", "Expert", "Student"]
|
||||
default_demo = config_blog_chars.get("blog_demographic", "Professional")
|
||||
default_demo_index = demographic_options.index(default_demo) if default_demo in demographic_options else 0
|
||||
|
||||
blog_demographic = st.selectbox(
|
||||
"Target Audience",
|
||||
options=demographic_options,
|
||||
index=default_demo_index,
|
||||
help="Who your blog content is primarily written for"
|
||||
)
|
||||
|
||||
# Blog type
|
||||
type_options = ["Informational", "How-to", "List", "Review", "Tutorial", "Opinion"]
|
||||
default_type = config_blog_chars.get("blog_type", "Informational")
|
||||
default_type_index = type_options.index(default_type) if default_type in type_options else 0
|
||||
|
||||
blog_type = st.selectbox(
|
||||
"Blog Type",
|
||||
options=type_options,
|
||||
index=default_type_index,
|
||||
help="The format and purpose of your blog content"
|
||||
)
|
||||
|
||||
# Blog language
|
||||
language_options = ["English", "Spanish", "French", "German", "Italian", "Portuguese"]
|
||||
default_lang = config_blog_chars.get("blog_language", "English")
|
||||
default_lang_index = language_options.index(default_lang) if default_lang in language_options else 0
|
||||
|
||||
blog_language = st.selectbox(
|
||||
"Blog Language",
|
||||
options=language_options,
|
||||
index=default_lang_index,
|
||||
help="The language your blog will be written in"
|
||||
)
|
||||
|
||||
# Blog output format
|
||||
format_options = ["markdown", "html", "plain text"]
|
||||
default_format = config_blog_chars.get("blog_output_format", "markdown").lower()
|
||||
default_format_index = format_options.index(default_format) if default_format in format_options else 0
|
||||
|
||||
blog_output_format = st.selectbox(
|
||||
"Output Format",
|
||||
options=format_options,
|
||||
index=default_format_index,
|
||||
help="The format in which the blog content will be generated"
|
||||
)
|
||||
|
||||
# Show current configuration source
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
st.success(f"✅ Using blog characteristics from configuration file")
|
||||
else:
|
||||
st.info("ℹ️ Using default blog characteristics (no configuration file found)")
|
||||
|
||||
return {
|
||||
"blog_length": blog_length,
|
||||
"blog_tone": blog_tone,
|
||||
"blog_demographic": blog_demographic,
|
||||
"blog_type": blog_type,
|
||||
"blog_language": blog_language,
|
||||
"blog_output_format": blog_output_format
|
||||
}
|
||||
|
||||
|
||||
def display_content_analysis_tab():
|
||||
"""Display the Content & Analysis Options tab and return the selected options."""
|
||||
st.markdown("#### Content & Analysis Options")
|
||||
|
||||
# Create two columns for better organization
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("**Content Enhancements**")
|
||||
create_seo_tags = st.checkbox(
|
||||
'✅ Generate SEO metadata',
|
||||
value=True,
|
||||
help='Create schema markup, meta tags, and social media metadata'
|
||||
)
|
||||
generate_social_media = st.checkbox(
|
||||
'✅ Create social media posts',
|
||||
value=False,
|
||||
help="Generate matching social content for Facebook, Twitter, and LinkedIn"
|
||||
)
|
||||
add_table_of_contents = st.checkbox(
|
||||
'✅ Add table of contents',
|
||||
value=True,
|
||||
help="Include an auto-generated table of contents at the beginning of the blog"
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.markdown("**Analysis & Improvement**")
|
||||
content_analysis = st.checkbox(
|
||||
'✅ Perform content analysis',
|
||||
value=False,
|
||||
help="Include proofreading, readability score, and improvement suggestions"
|
||||
)
|
||||
enhance_readability = st.checkbox(
|
||||
'✅ Enhance readability',
|
||||
value=True,
|
||||
help="Optimize sentence structure and vocabulary for better readability"
|
||||
)
|
||||
fact_checking = st.checkbox(
|
||||
'✅ Basic fact verification',
|
||||
value=False,
|
||||
help="Verify key facts from multiple sources when possible"
|
||||
)
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown("**Formatting Options**")
|
||||
|
||||
# Create two columns for formatting options
|
||||
fmt_col1, fmt_col2 = st.columns(2)
|
||||
|
||||
with fmt_col1:
|
||||
section_headings = st.checkbox(
|
||||
'✅ Use section headings',
|
||||
value=True,
|
||||
help="Include clear section headings throughout the blog"
|
||||
)
|
||||
include_lists = st.checkbox(
|
||||
'✅ Use bullet points and lists',
|
||||
value=True,
|
||||
help="Format appropriate content as bullet points or numbered lists"
|
||||
)
|
||||
|
||||
with fmt_col2:
|
||||
include_quotes = st.checkbox(
|
||||
'✅ Include relevant quotes',
|
||||
value=False,
|
||||
help="Add expert quotes or important statements as blockquotes"
|
||||
)
|
||||
use_subheadings = st.checkbox(
|
||||
'✅ Use subheadings',
|
||||
value=True,
|
||||
help="Break down sections with descriptive subheadings"
|
||||
)
|
||||
|
||||
return {
|
||||
"create_seo_tags": create_seo_tags,
|
||||
"generate_social_media": generate_social_media,
|
||||
"add_table_of_contents": add_table_of_contents,
|
||||
"content_analysis": content_analysis,
|
||||
"enhance_readability": enhance_readability,
|
||||
"fact_checking": fact_checking,
|
||||
"section_headings": section_headings,
|
||||
"include_lists": include_lists,
|
||||
"include_quotes": include_quotes,
|
||||
"use_subheadings": use_subheadings
|
||||
}
|
||||
|
||||
|
||||
def display_blog_images_tab():
|
||||
"""Display the Blog Images Details tab and return the selected options."""
|
||||
st.markdown("#### Blog Images Settings")
|
||||
|
||||
# Load default values from configuration
|
||||
config_images = get_blog_images_from_config()
|
||||
|
||||
# Image generation model selection
|
||||
model_options = ["stable-diffusion", "dall-e", "midjourney", "imagen"]
|
||||
default_model = config_images.get("image_model", "stable-diffusion")
|
||||
default_model_index = model_options.index(default_model) if default_model in model_options else 0
|
||||
|
||||
image_model = st.selectbox(
|
||||
"Image Generation Model",
|
||||
options=model_options,
|
||||
index=default_model_index,
|
||||
help="AI model used to generate blog images"
|
||||
)
|
||||
|
||||
# Number of blog images
|
||||
num_images = st.number_input(
|
||||
"Number of Blog Images",
|
||||
min_value=0,
|
||||
max_value=10,
|
||||
value=config_images.get("num_images", 1),
|
||||
step=1,
|
||||
help="Number of images to generate for the blog"
|
||||
)
|
||||
|
||||
# Image style
|
||||
style_options = ["Realistic", "Artistic", "Cartoon", "Minimalist", "Corporate", "Vibrant"]
|
||||
default_style = config_images.get("image_style", "Realistic")
|
||||
default_style_index = style_options.index(default_style) if default_style in style_options else 0
|
||||
|
||||
image_style = st.selectbox(
|
||||
"Image Style",
|
||||
options=style_options,
|
||||
index=default_style_index,
|
||||
help="Visual style of the generated images"
|
||||
)
|
||||
|
||||
# Additional image options
|
||||
st.markdown("**Additional Image Options**")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
generate_featured = st.checkbox(
|
||||
'✅ Generate featured image',
|
||||
value=True,
|
||||
help="Create a featured header image for the blog"
|
||||
)
|
||||
add_captions = st.checkbox(
|
||||
'✅ Add image captions',
|
||||
value=True,
|
||||
help="Generate descriptive captions for each image"
|
||||
)
|
||||
|
||||
with col2:
|
||||
use_alt_text = st.checkbox(
|
||||
'✅ Generate alt text',
|
||||
value=True,
|
||||
help="Create accessibility alt text for all images"
|
||||
)
|
||||
optimize_images = st.checkbox(
|
||||
'✅ Optimize image placement',
|
||||
value=True,
|
||||
help="Intelligently place images throughout the content"
|
||||
)
|
||||
|
||||
# Show current configuration source
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
st.success(f"✅ Using image settings from configuration file")
|
||||
else:
|
||||
st.info("ℹ️ Using default image settings (no configuration file found)")
|
||||
|
||||
return {
|
||||
"image_model": image_model,
|
||||
"num_images": num_images,
|
||||
"image_style": image_style,
|
||||
"generate_featured": generate_featured,
|
||||
"add_captions": add_captions,
|
||||
"use_alt_text": use_alt_text,
|
||||
"optimize_placement": optimize_images
|
||||
}
|
||||
|
||||
|
||||
def display_llm_options_tab():
|
||||
"""Display the LLM Options tab and return the selected options."""
|
||||
st.markdown("#### Language Model Settings")
|
||||
|
||||
# Load default values from configuration
|
||||
config_llm = get_llm_options_from_config()
|
||||
|
||||
# LLM provider selection
|
||||
provider_options = ["google", "openai", "anthropic", "local"]
|
||||
default_provider = config_llm.get("provider", "google")
|
||||
default_provider_index = provider_options.index(default_provider) if default_provider in provider_options else 0
|
||||
|
||||
llm_provider = st.selectbox(
|
||||
"AI Provider",
|
||||
options=provider_options,
|
||||
index=default_provider_index,
|
||||
help="The AI provider to use for content generation"
|
||||
)
|
||||
|
||||
# Model selection (dynamic based on provider)
|
||||
if llm_provider == "google":
|
||||
model_options = ["gemini-1.5-flash-latest", "gemini-1.5-pro-latest", "gemini-pro"]
|
||||
elif llm_provider == "openai":
|
||||
model_options = ["gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"]
|
||||
elif llm_provider == "anthropic":
|
||||
model_options = ["claude-3-opus", "claude-3-sonnet", "claude-3-haiku"]
|
||||
else:
|
||||
model_options = ["llama-3-70b", "mistral-large", "local-model"]
|
||||
|
||||
default_model = config_llm.get("model", "gemini-1.5-flash-latest")
|
||||
default_model_index = 0
|
||||
if default_model in model_options:
|
||||
default_model_index = model_options.index(default_model)
|
||||
|
||||
llm_model = st.selectbox(
|
||||
"AI Model",
|
||||
options=model_options,
|
||||
index=default_model_index,
|
||||
help="The specific AI model to use for content generation"
|
||||
)
|
||||
|
||||
# Create two columns for temperature and max tokens
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
# Temperature setting
|
||||
temperature = st.slider(
|
||||
"Temperature",
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
value=config_llm.get("temperature", 0.7),
|
||||
step=0.1,
|
||||
help="Controls randomness: lower values are more deterministic, higher values more creative"
|
||||
)
|
||||
|
||||
with col2:
|
||||
# Max tokens
|
||||
max_tokens = st.number_input(
|
||||
"Max Tokens",
|
||||
min_value=1000,
|
||||
max_value=32000,
|
||||
value=config_llm.get("max_tokens", 4000),
|
||||
step=1000,
|
||||
help="Maximum length of generated content (in tokens)"
|
||||
)
|
||||
|
||||
# Advanced LLM options
|
||||
st.markdown("---")
|
||||
st.markdown("**Advanced LLM Options**")
|
||||
show_advanced_llm = st.checkbox("Show advanced LLM parameters", value=False)
|
||||
|
||||
advanced_params = {}
|
||||
if show_advanced_llm:
|
||||
# Top-p (nucleus sampling)
|
||||
top_p = st.slider(
|
||||
"Top-p (Nucleus Sampling)",
|
||||
min_value=0.1,
|
||||
max_value=1.0,
|
||||
value=0.9,
|
||||
step=0.1,
|
||||
help="Controls diversity via nucleus sampling: 1.0 considers all tokens, lower values restrict to more likely tokens"
|
||||
)
|
||||
|
||||
# Top-k
|
||||
top_k = st.slider(
|
||||
"Top-k",
|
||||
min_value=1,
|
||||
max_value=100,
|
||||
value=40,
|
||||
step=1,
|
||||
help="Controls diversity by limiting to top k tokens: higher values allow more diversity"
|
||||
)
|
||||
|
||||
# Presence penalty
|
||||
presence_penalty = st.slider(
|
||||
"Presence Penalty",
|
||||
min_value=-2.0,
|
||||
max_value=2.0,
|
||||
value=0.0,
|
||||
step=0.1,
|
||||
help="Penalizes repeated tokens: positive values discourage repetition"
|
||||
)
|
||||
|
||||
advanced_params = {
|
||||
"top_p": top_p,
|
||||
"top_k": top_k,
|
||||
"presence_penalty": presence_penalty
|
||||
}
|
||||
|
||||
# Show current configuration source
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
st.success(f"✅ Using LLM settings from configuration file")
|
||||
else:
|
||||
st.info("ℹ️ Using default LLM settings (no configuration file found)")
|
||||
|
||||
return {
|
||||
"provider": llm_provider,
|
||||
"model": llm_model,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
**advanced_params
|
||||
}
|
||||
|
||||
|
||||
def display_search_settings_tab():
|
||||
"""Display the Search Settings tab and return the selected options."""
|
||||
st.markdown("#### AI Search Configuration")
|
||||
st.markdown("Control how the AI researches your topic")
|
||||
|
||||
# Load default values from configuration
|
||||
config_search_params = get_search_params_from_config()
|
||||
|
||||
# Number of search results
|
||||
max_results = st.slider(
|
||||
"Maximum Results",
|
||||
min_value=5,
|
||||
max_value=30,
|
||||
value=config_search_params.get("max_results", 10),
|
||||
step=5,
|
||||
help="Maximum number of search results to use for research"
|
||||
)
|
||||
|
||||
# Search depth
|
||||
search_depth = st.radio(
|
||||
"Search Depth",
|
||||
options=["basic", "advanced"],
|
||||
index=0,
|
||||
horizontal=True,
|
||||
help="Basic: Faster but less comprehensive. Advanced: More thorough but slower."
|
||||
)
|
||||
|
||||
# Include domains
|
||||
include_domains = st.text_input(
|
||||
"Include Domains (Optional)",
|
||||
value="",
|
||||
help="Comma-separated list of domains to prioritize in search (e.g., wikipedia.org,nih.gov)"
|
||||
)
|
||||
|
||||
# Time range - use value from config
|
||||
time_options = ["day", "week", "month", "year", "all"]
|
||||
default_time_index = time_options.index(config_search_params.get("time_range", "year")) if config_search_params.get("time_range", "year") in time_options else 3 # Default to "year" (index 3)
|
||||
|
||||
time_range = st.select_slider(
|
||||
"Time Range",
|
||||
options=time_options,
|
||||
value=time_options[default_time_index],
|
||||
help="Limit search results to a specific time period"
|
||||
)
|
||||
|
||||
# Show current configuration source
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
st.success(f"✅ Using search defaults from configuration file")
|
||||
else:
|
||||
st.info("ℹ️ Using default search settings (no configuration file found)")
|
||||
|
||||
# Replace expander with checkbox for configuration display
|
||||
show_config = st.checkbox("Show configuration details", value=False)
|
||||
if show_config:
|
||||
st.markdown("""
|
||||
**Configuration File Location**
|
||||
Search parameters are loaded from the main configuration file at:
|
||||
`lib/workspace/alwrity_config/main_config.json`
|
||||
|
||||
You can modify this file to change the default search settings.
|
||||
""")
|
||||
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
try:
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
config_content = f.read()
|
||||
st.code(config_content, language="json")
|
||||
except:
|
||||
st.warning("Could not read configuration file")
|
||||
|
||||
st.info("These settings control how the AI performs web research for your content. More thorough searches may take longer but produce better results.")
|
||||
|
||||
# Process include_domains from string to list if provided
|
||||
domains_list = []
|
||||
if include_domains:
|
||||
domains_list = [domain.strip() for domain in include_domains.split(",") if domain.strip()]
|
||||
|
||||
return {
|
||||
"max_results": max_results,
|
||||
"search_depth": search_depth,
|
||||
"time_range": time_range,
|
||||
"include_domains": domains_list
|
||||
}
|
||||
|
||||
|
||||
def display_advanced_options():
|
||||
"""Display all advanced options tabs and return the selected configurations."""
|
||||
|
||||
with st.expander("⚙️ Advanced Options for Personalization, Analysis, Images, LLM, and Search", expanded=False):
|
||||
content_type, selected_content_type = display_content_type_selection(inside_expander=True)
|
||||
|
||||
tabs = st.tabs(["Personalization", "Analysis Options", "Blog Images Details", "LLM Options", "Search Settings"])
|
||||
|
||||
with tabs[0]: # Content Characteristics
|
||||
blog_params = display_content_characteristics_tab()
|
||||
|
||||
with tabs[1]: # Combined Content & Analysis Options
|
||||
content_analysis_params = display_content_analysis_tab()
|
||||
|
||||
with tabs[2]: # Blog Images Details
|
||||
image_params = display_blog_images_tab()
|
||||
|
||||
with tabs[3]: # LLM Options
|
||||
llm_params = display_llm_options_tab()
|
||||
|
||||
with tabs[4]: # Search Settings
|
||||
search_params = display_search_settings_tab()
|
||||
|
||||
return content_type, selected_content_type, blog_params, content_analysis_params, image_params, llm_params, search_params
|
||||
|
||||
|
||||
def blog_from_keyword():
|
||||
"""Input blog keywords, research and write a factual blog with enhanced UI."""
|
||||
|
||||
# Get user inputs
|
||||
user_input, uploaded_file, audio_input = display_input_section()
|
||||
|
||||
# Display advanced options and get configurations
|
||||
content_type, selected_content_type, blog_params, content_analysis_params, image_params, llm_params, search_params = display_advanced_options()
|
||||
|
||||
# Generate button with icon and clearer purpose
|
||||
st.markdown("") # Add spacing
|
||||
generate_pressed = st.button("✨ Generate Blog Content", use_container_width=True)
|
||||
|
||||
# Processing logic
|
||||
if generate_pressed:
|
||||
st.empty()
|
||||
|
||||
if not uploaded_file and not user_input and not audio_input:
|
||||
st.error("Please provide at least one input source (keywords, file, or voice recording)")
|
||||
st.stop()
|
||||
|
||||
input_type = process_input(user_input, uploaded_file)
|
||||
|
||||
# Use the utility function to handle content generation
|
||||
handle_content_generation(input_type, user_input, uploaded_file, search_params, blog_params, selected_content_type)
|
||||
|
||||
|
||||
def ai_blog_writer_page():
|
||||
"""Render the AI Blog Writer page with enhanced styling."""
|
||||
logger.info("Rendering AI Blog Writer page")
|
||||
|
||||
# Apply shared blog writer styles
|
||||
apply_blog_writer_styles()
|
||||
|
||||
# Back button with icon
|
||||
if st.button("← Back to Dashboard", key="back_to_dashboard"):
|
||||
logger.info("User clicked back button, returning to ai writer dashboard")
|
||||
st.query_params.clear()
|
||||
st.rerun()
|
||||
|
||||
# Enhanced header with icon
|
||||
st.markdown("""
|
||||
<div class="page-header">
|
||||
<h1>✍️ AI Blog Writer</h1>
|
||||
<p>Create engaging, SEO-optimized blog content with AI assistance. Our advanced algorithms help you generate high-quality, relevant articles for any topic or niche.</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Call the blog generator function with enhanced UI
|
||||
logger.info("Calling blog_from_keyword function")
|
||||
blog_from_keyword()
|
||||
|
||||
logger.info("Finished rendering AI Blog Writer page")
|
||||
@@ -1,867 +0,0 @@
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
from loguru import logger
|
||||
import PyPDF2
|
||||
import streamlit as st
|
||||
import tiktoken
|
||||
import openai
|
||||
from datetime import datetime
|
||||
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
# Remove the circular import
|
||||
# from lib.ai_writers.ai_blog_writer.keywords_to_blog_streamlit import write_blog_from_keywords
|
||||
from lib.ai_writers.speech_to_blog.main_audio_to_blog import generate_audio_blog
|
||||
from lib.ai_writers.long_form_ai_writer import long_form_generator
|
||||
from lib.ai_writers.web_url_ai_writer import blog_from_url
|
||||
from lib.ai_writers.image_ai_writer import blog_from_image
|
||||
from .blog_from_google_serp import write_blog_google_serp
|
||||
from lib.blog_metadata.get_blog_metadata import blog_metadata
|
||||
from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
||||
|
||||
# Constants
|
||||
CONFIG_PATH = os.path.join("lib", "workspace", "alwrity_config", "main_config.json")
|
||||
DEFAULT_CONFIG = {
|
||||
"Search Engine Parameters": {
|
||||
"Geographic Location": "us",
|
||||
"Search Language": "en",
|
||||
"Number of Results": 10,
|
||||
"Time Range": "year"
|
||||
}
|
||||
}
|
||||
|
||||
# Function to load configuration from JSON file
|
||||
def load_config():
|
||||
"""Load configuration from the main config JSON file."""
|
||||
try:
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
config = json.load(f)
|
||||
logger.info(f"Loaded configuration from {CONFIG_PATH}")
|
||||
return config
|
||||
else:
|
||||
logger.warning(f"Configuration file not found at {CONFIG_PATH}, using defaults")
|
||||
return DEFAULT_CONFIG
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading configuration: {str(e)}")
|
||||
return DEFAULT_CONFIG
|
||||
|
||||
# Function to get search parameters from config
|
||||
def get_search_params_from_config():
|
||||
"""Extract search parameters from the main configuration."""
|
||||
config = load_config()
|
||||
search_params = config.get("Search Engine Parameters", {})
|
||||
|
||||
# Map config values to expected parameter names
|
||||
result = {
|
||||
"max_results": search_params.get("Number of Results", 10),
|
||||
"time_range": search_params.get("Time Range", "year").lower(),
|
||||
"geo": search_params.get("Geographic Location", "us"),
|
||||
"language": search_params.get("Search Language", "en")
|
||||
}
|
||||
|
||||
# Normalize time_range to match our options
|
||||
time_map = {
|
||||
"day": "day",
|
||||
"week": "week",
|
||||
"month": "month",
|
||||
"year": "year",
|
||||
"anytime": "all",
|
||||
"all": "all"
|
||||
}
|
||||
result["time_range"] = time_map.get(result["time_range"].lower(), "year")
|
||||
|
||||
logger.info(f"Using search parameters from config: {result}")
|
||||
return result
|
||||
|
||||
# Function to get blog content characteristics from config
|
||||
def get_blog_characteristics_from_config():
|
||||
"""Extract blog content characteristics from the main configuration."""
|
||||
config = load_config()
|
||||
blog_characteristics = config.get("Blog Content Characteristics", {})
|
||||
|
||||
# Map config values to expected parameter names
|
||||
result = {
|
||||
"blog_length": blog_characteristics.get("Blog Length", "2000"),
|
||||
"blog_tone": blog_characteristics.get("Blog Tone", "Professional"),
|
||||
"blog_demographic": blog_characteristics.get("Blog Demographic", "Professional"),
|
||||
"blog_type": blog_characteristics.get("Blog Type", "Informational"),
|
||||
"blog_language": blog_characteristics.get("Blog Language", "English"),
|
||||
"blog_output_format": blog_characteristics.get("Blog Output Format", "markdown")
|
||||
}
|
||||
|
||||
logger.info(f"Using blog characteristics from config: {result}")
|
||||
return result
|
||||
|
||||
# Function to get blog image details from config
|
||||
def get_blog_images_from_config():
|
||||
"""Extract blog image details from the main configuration."""
|
||||
config = load_config()
|
||||
blog_images = config.get("Blog Images Details", {})
|
||||
|
||||
# Map config values to expected parameter names
|
||||
result = {
|
||||
"image_model": blog_images.get("Image Generation Model", "stable-diffusion"),
|
||||
"num_images": int(blog_images.get("Number of Blog Images", 1)),
|
||||
"image_style": blog_images.get("Image Style", "Realistic")
|
||||
}
|
||||
|
||||
logger.info(f"Using blog image details from config: {result}")
|
||||
return result
|
||||
|
||||
# Function to get LLM options from config
|
||||
def get_llm_options_from_config():
|
||||
"""Extract LLM options from the main configuration."""
|
||||
config = load_config()
|
||||
llm_options = config.get("LLM Options", {})
|
||||
|
||||
# Map config values to expected parameter names
|
||||
result = {
|
||||
"provider": llm_options.get("GPT Provider", "google"),
|
||||
"model": llm_options.get("Model", "gemini-1.5-flash-latest"),
|
||||
"temperature": float(llm_options.get("Temperature", 0.7)),
|
||||
"max_tokens": int(llm_options.get("Max Tokens", 4000))
|
||||
}
|
||||
|
||||
logger.info(f"Using LLM options from config: {result}")
|
||||
return result
|
||||
|
||||
# Split a text into smaller chunks of size n, preferably ending at the end of a sentence
|
||||
def create_chunks(text, n, tokenizer):
|
||||
tokens = tokenizer.encode(text)
|
||||
"""Yield successive n-sized chunks from text."""
|
||||
i = 0
|
||||
while i < len(tokens):
|
||||
# Find the nearest end of sentence within a range of 0.5 * n and 1.5 * n tokens
|
||||
j = min(i + int(1.5 * n), len(tokens))
|
||||
while j > i + int(0.5 * n):
|
||||
# Decode the tokens and check for full stop or newline
|
||||
chunk = tokenizer.decode(tokens[i:j])
|
||||
if chunk.endswith(".") or chunk.endswith("\n"):
|
||||
break
|
||||
j -= 1
|
||||
# If no end of sentence found, use n tokens as the chunk size
|
||||
if j == i + int(0.5 * n):
|
||||
j = min(i + n, len(tokens))
|
||||
yield tokens[i:j]
|
||||
i = j
|
||||
|
||||
|
||||
def extract_chunk(document, template_prompt):
|
||||
""" Chunking for large documents, exceed context window"""
|
||||
prompt = template_prompt.replace('<document>', document)
|
||||
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to get response from LLM: {err}")
|
||||
raise
|
||||
|
||||
|
||||
def blog_from_pdf(pdf_text):
|
||||
"""
|
||||
Load in a long PDF and extract key information.
|
||||
Chunk up document and process each chunk, then combine them.
|
||||
"""
|
||||
template_prompt=f'''Extract key pieces of information from the given document.
|
||||
|
||||
When you extract a key piece of information, include the closest page number.
|
||||
Ex: Extracted Information (Page number)
|
||||
\n\nDocument: \"\"\"<document>\"\"\"\n\n'''
|
||||
|
||||
# Initialize tokenizer
|
||||
tokenizer = tiktoken.get_encoding("cl100k_base")
|
||||
results = []
|
||||
|
||||
chunks = create_chunks(pdf_text, 1000, tokenizer)
|
||||
text_chunks = [tokenizer.decode(chunk) for chunk in chunks]
|
||||
|
||||
for chunk in text_chunks:
|
||||
try:
|
||||
results.append(extract_chunk(chunk, template_prompt))
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing chunk: {e}")
|
||||
# Continue with other chunks even if one fails
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# Input validation functions
|
||||
def is_youtube_link(text):
|
||||
"""Check if text is a valid YouTube link."""
|
||||
if text is not None:
|
||||
youtube_regex = re.compile(r'(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})')
|
||||
return youtube_regex.match(text)
|
||||
return False
|
||||
|
||||
|
||||
def is_web_link(text):
|
||||
"""Check if text is a valid web link."""
|
||||
if text is not None:
|
||||
web_regex = re.compile(r'(https?://)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)')
|
||||
return web_regex.match(text)
|
||||
return False
|
||||
|
||||
|
||||
def process_input(input_text, uploaded_file):
|
||||
"""
|
||||
Determine the type of input provided by the user.
|
||||
|
||||
Args:
|
||||
input_text (str): The text input from the user
|
||||
uploaded_file: The file uploaded by the user
|
||||
|
||||
Returns:
|
||||
str: The determined input type ("youtube_url", "web_url", "keywords", "PDF_file", "image_file", "audio_file", "video_file", or None)
|
||||
"""
|
||||
# Process text input
|
||||
if input_text:
|
||||
if is_youtube_link(input_text):
|
||||
if input_text.startswith("https://www.youtube.com/") or input_text.startswith("http://www.youtube.com/"):
|
||||
return "youtube_url"
|
||||
else:
|
||||
st.error("Invalid YouTube URL. Please enter a valid URL.")
|
||||
return None
|
||||
elif is_web_link(input_text):
|
||||
return "web_url"
|
||||
else:
|
||||
return "keywords"
|
||||
|
||||
# Process file input
|
||||
if uploaded_file is not None:
|
||||
file_details = {"filename": uploaded_file.name, "filetype": uploaded_file.type}
|
||||
st.write(file_details)
|
||||
|
||||
# Handle different file types
|
||||
if uploaded_file.type.startswith("text/"):
|
||||
content = uploaded_file.read().decode("utf-8")
|
||||
st.text(content)
|
||||
return "text_file"
|
||||
elif uploaded_file.type == "application/pdf":
|
||||
return "PDF_file"
|
||||
elif uploaded_file.type in ["application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/msword"]:
|
||||
st.write("Word document uploaded. Add your DOCX processing logic here.")
|
||||
return "word_file"
|
||||
elif uploaded_file.type.startswith("image/"):
|
||||
st.image(uploaded_file)
|
||||
return "image_file"
|
||||
elif uploaded_file.type.startswith("audio/"):
|
||||
st.audio(uploaded_file)
|
||||
return "audio_file"
|
||||
elif uploaded_file.type.startswith("video/"):
|
||||
st.video(uploaded_file)
|
||||
return "video_file"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Content processing functions
|
||||
def process_keywords_input(user_input, search_params, blog_params, selected_content_type):
|
||||
"""Process keywords input and generate content based on the selected options."""
|
||||
if not user_input or len(user_input.split()) < 2:
|
||||
st.error('Please provide at least two keywords for best results')
|
||||
return False
|
||||
|
||||
# Check for dialog states and handle them directly
|
||||
if st.session_state.get("show_title_dialog", False):
|
||||
st.warning("Please use the main function to handle title refinement dialog")
|
||||
# Clear the dialog state to avoid getting stuck
|
||||
st.session_state.show_title_dialog = False
|
||||
return False
|
||||
|
||||
if st.session_state.get("show_meta_dialog", False):
|
||||
st.warning("Please use the main function to handle meta description refinement dialog")
|
||||
# Clear the dialog state to avoid getting stuck
|
||||
st.session_state.show_meta_dialog = False
|
||||
return False
|
||||
|
||||
if st.session_state.get("show_snippet_dialog", False):
|
||||
st.warning("Please use the main function to handle structured data dialog")
|
||||
# Clear the dialog state to avoid getting stuck
|
||||
st.session_state.show_snippet_dialog = False
|
||||
return False
|
||||
|
||||
try:
|
||||
if selected_content_type == "Normal-length content":
|
||||
st.subheader("Your Generated Blog Post")
|
||||
logger.info(f"Generating standard blog post with parameters: {blog_params}")
|
||||
|
||||
# Use a direct approach to generate blog content to avoid nested expanders
|
||||
# Instead of importing write_blog_from_keywords which contains many expanders
|
||||
try:
|
||||
# Show simplified progress UI
|
||||
progress_container = st.container()
|
||||
with progress_container:
|
||||
progress_bar = st.progress(0)
|
||||
status_text = st.empty()
|
||||
|
||||
# Step 1: Initialize and show progress
|
||||
status_text.info("Initializing blog generation...")
|
||||
progress_bar.progress(0.1)
|
||||
|
||||
# Initialize parameters
|
||||
from .blog_ai_research_utils import initialize_parameters
|
||||
search_params, blog_params = initialize_parameters(search_params, blog_params)
|
||||
|
||||
# Step 2: Research phase
|
||||
status_text.info("Researching your topic...")
|
||||
progress_bar.progress(0.2)
|
||||
|
||||
# Perform research using direct function calls
|
||||
from .blog_ai_research_utils import do_google_serp_search, do_tavily_ai_search
|
||||
|
||||
# Do Google search
|
||||
status_text.info("Searching Google for relevant information...")
|
||||
google_result = do_google_serp_search(user_input, max_results=search_params.get("max_results", 10))
|
||||
google_success = google_result and 'results' in google_result and google_result['results']
|
||||
progress_bar.progress(0.4)
|
||||
|
||||
# Do Tavily search if needed
|
||||
tavily_result = None
|
||||
tavily_success = False
|
||||
if not google_success:
|
||||
status_text.info("Performing additional research with Tavily...")
|
||||
tavily_result, _, _ = do_tavily_ai_search(
|
||||
user_input,
|
||||
max_results=search_params.get("max_results", 10),
|
||||
search_depth=search_params.get("search_depth", "basic")
|
||||
)
|
||||
tavily_success = tavily_result is not None
|
||||
progress_bar.progress(0.5)
|
||||
|
||||
# Step 3: Generate content
|
||||
status_text.info("Generating blog content...")
|
||||
progress_bar.progress(0.6)
|
||||
|
||||
# Generate content based on search results
|
||||
from .blog_from_google_serp import write_blog_google_serp
|
||||
|
||||
if google_success:
|
||||
blog_content = write_blog_google_serp(user_input, google_result['results'], blog_params=blog_params)
|
||||
elif tavily_success:
|
||||
blog_content = write_blog_google_serp(user_input, tavily_result, blog_params=blog_params)
|
||||
else:
|
||||
status_text.error("Failed to gather research data. Please try again.")
|
||||
return False
|
||||
|
||||
# Step 4: Generate metadata and image
|
||||
status_text.info("Adding metadata and final touches...")
|
||||
progress_bar.progress(0.8)
|
||||
|
||||
# Import functions from keywords_to_blog_streamlit
|
||||
from .keywords_to_blog_streamlit import generate_audio_version
|
||||
|
||||
# Define a simple update_progress function for compatibility
|
||||
def simple_update_progress(step, total, message):
|
||||
status_text.info(message)
|
||||
progress_bar.progress(step / total)
|
||||
|
||||
# Generate metadata and image
|
||||
# Import only essential functions needed for core processing
|
||||
from .ai_blog_generator_utils import generate_blog_metadata, generate_blog_image
|
||||
try:
|
||||
# Create a proper status object
|
||||
with st.status("Generating metadata and image...", expanded=True) as status:
|
||||
# Generate metadata
|
||||
blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = generate_blog_metadata(
|
||||
blog_content, user_input, status)
|
||||
|
||||
# Generate featured image if metadata is available
|
||||
generated_image_filepath = None
|
||||
if blog_title and blog_meta_desc:
|
||||
generated_image_filepath = generate_blog_image(
|
||||
blog_title, blog_meta_desc, blog_content, status, blog_tags)
|
||||
|
||||
# Save blog content to file
|
||||
saved_blog_to_file = None
|
||||
from ...blog_postprocessing.save_blog_to_file import save_blog_to_file
|
||||
if blog_title and blog_meta_desc:
|
||||
saved_blog_to_file = save_blog_to_file(
|
||||
blog_content, blog_title, blog_meta_desc, blog_tags,
|
||||
blog_categories, generated_image_filepath)
|
||||
|
||||
# Create metadata dictionary with string conversions for table display
|
||||
metadata = {
|
||||
"blog_title": blog_title or "",
|
||||
"blog_meta_desc": blog_meta_desc or "",
|
||||
"blog_tags": ", ".join(blog_tags) if isinstance(blog_tags, list) else str(blog_tags or ""),
|
||||
"blog_categories": ", ".join(blog_categories) if isinstance(blog_categories, list) else str(blog_categories or ""),
|
||||
"blog_hashtags": blog_hashtags or "",
|
||||
"blog_slug": blog_slug or ""
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating metadata or image: {e}")
|
||||
metadata = {
|
||||
"blog_title": "Generated Blog",
|
||||
"blog_meta_desc": "",
|
||||
"blog_tags": "",
|
||||
"blog_categories": "",
|
||||
"blog_hashtags": "",
|
||||
"blog_slug": ""
|
||||
}
|
||||
generated_image_filepath = None
|
||||
saved_blog_to_file = None
|
||||
|
||||
# Clear progress indicators
|
||||
progress_bar.empty()
|
||||
status_text.empty()
|
||||
|
||||
# Final message
|
||||
final_message = st.empty()
|
||||
final_message.success("Blog generation complete!")
|
||||
|
||||
# Display blog content first (without using expanders)
|
||||
st.markdown("## Content")
|
||||
st.markdown(blog_content)
|
||||
|
||||
# Show file save information if available
|
||||
if saved_blog_to_file:
|
||||
st.success(f"✅ Blog saved to: {saved_blog_to_file}")
|
||||
|
||||
# Add the audio generation button
|
||||
st.markdown("---")
|
||||
audio_col1, audio_col2 = st.columns([1, 3])
|
||||
with audio_col1:
|
||||
generate_audio_button = st.button("🔊 Generate Audio Version", use_container_width=True)
|
||||
|
||||
with audio_col2:
|
||||
if generate_audio_button:
|
||||
generate_audio_version(blog_content)
|
||||
|
||||
# Display metadata success message
|
||||
if metadata["blog_title"]:
|
||||
st.success(f"✅ Generated metadata for: {metadata['blog_title']}")
|
||||
|
||||
# Display metadata table (without nesting expanders)
|
||||
st.markdown("---")
|
||||
st.subheader("🏷️ Blog SEO Metadata")
|
||||
st.table({
|
||||
"Metadata": ["Blog Title", "Meta Description", "Tags", "Categories", "Hashtags", "Slug"],
|
||||
"Value": [
|
||||
metadata["blog_title"],
|
||||
metadata["blog_meta_desc"],
|
||||
metadata["blog_tags"],
|
||||
metadata["blog_categories"],
|
||||
metadata["blog_hashtags"],
|
||||
metadata["blog_slug"]
|
||||
]
|
||||
})
|
||||
|
||||
# Display image if available
|
||||
if generated_image_filepath:
|
||||
st.subheader("🖼️ Featured Image")
|
||||
st.image(generated_image_filepath, caption=metadata["blog_title"] or "Featured Image", use_column_width=True)
|
||||
|
||||
# Add regenerate button
|
||||
if st.button("🔄 Regenerate Image", key="regenerate_image_simplified"):
|
||||
# Use the function directly to avoid any nested expanders
|
||||
new_image_path = regenerate_blog_image(
|
||||
metadata["blog_title"],
|
||||
metadata["blog_meta_desc"],
|
||||
blog_content,
|
||||
metadata["blog_tags"]
|
||||
)
|
||||
if new_image_path:
|
||||
st.success("✅ Image regenerated successfully!")
|
||||
st.image(new_image_path, caption=metadata["blog_title"], use_column_width=True)
|
||||
else:
|
||||
st.subheader("🖼️ Featured Image")
|
||||
st.info("No image was generated. Try regenerating the blog.")
|
||||
|
||||
# Add refinement buttons directly, without using helper functions
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
if st.button("🔄 Refine Blog Title", key="refine_title_simplified", use_container_width=True):
|
||||
st.session_state.show_title_dialog = True
|
||||
st.rerun()
|
||||
with col2:
|
||||
if st.button("🔄 Refine Meta Description", key="refine_meta_simplified", use_container_width=True):
|
||||
st.session_state.show_meta_dialog = True
|
||||
st.rerun()
|
||||
|
||||
# Add structured data section directly, without using helper functions
|
||||
st.markdown("---")
|
||||
st.markdown("### Get Structured Data")
|
||||
|
||||
structured_data_col1, structured_data_col2 = st.columns([3, 1])
|
||||
with structured_data_col1:
|
||||
st.info("Rich snippets boost visibility and click-through rates in search results.")
|
||||
with structured_data_col2:
|
||||
if st.button("📊 Generate Rich Snippet", key="snippet_simplified", use_container_width=True):
|
||||
st.session_state.show_snippet_dialog = True
|
||||
st.rerun()
|
||||
|
||||
# Clear the success message after a delay
|
||||
import time
|
||||
time.sleep(3)
|
||||
final_message.empty()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as inner_err:
|
||||
logger.error(f"Error in simplified blog generation: {inner_err}")
|
||||
st.error(f"Failed to generate blog content: {inner_err}")
|
||||
return False
|
||||
|
||||
elif selected_content_type == "Long-form content":
|
||||
logger.info(f"Generating long-form content with parameters: {blog_params}")
|
||||
|
||||
# Ensure all blog parameters are properly passed to long-form generator
|
||||
long_form_generator(
|
||||
user_input,
|
||||
search_params=search_params,
|
||||
blog_params=blog_params
|
||||
)
|
||||
|
||||
# Show success message briefly then clear it
|
||||
success_msg = st.empty()
|
||||
success_msg.success(f"Successfully generated long-form content for: {user_input}")
|
||||
# Clear the message after 3 seconds
|
||||
import time
|
||||
time.sleep(3)
|
||||
success_msg.empty()
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
info_msg = st.empty()
|
||||
info_msg.info("AI Agent Team feature is coming soon! This will provide multi-perspective content with different AI experts collaborating on your blog.")
|
||||
return False
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"An error occurred while generating content: {err}")
|
||||
st.error(f"An error occurred while generating content: {err}")
|
||||
return False
|
||||
|
||||
|
||||
def process_pdf_input(uploaded_file):
|
||||
"""Process a PDF file and generate content."""
|
||||
# Replace expander with a container to avoid nested expanders
|
||||
pdf_container = st.container()
|
||||
with pdf_container:
|
||||
st.subheader("Processing PDF Document")
|
||||
pdf_reader = PyPDF2.PdfReader(uploaded_file)
|
||||
text = ""
|
||||
combined_result = ""
|
||||
|
||||
# Show progress with better UI
|
||||
progress_text = st.empty()
|
||||
progress_bar = st.progress(0)
|
||||
|
||||
total_pages = len(pdf_reader.pages)
|
||||
for page_num, page in enumerate(pdf_reader.pages):
|
||||
progress_text.text(f"Processing page {page_num+1}/{total_pages}")
|
||||
text += page.extract_text()
|
||||
text = text.replace("\n", " ")
|
||||
text = re.sub(r"(\w)([A-Z])", r"\1 \2", text)
|
||||
|
||||
results = blog_from_pdf(text)
|
||||
progress_percent = (page_num + 1) / total_pages
|
||||
progress_bar.progress(progress_percent)
|
||||
combined_result += str(results[-1])
|
||||
|
||||
progress_text.empty()
|
||||
progress_bar.empty()
|
||||
|
||||
st.subheader("Generated Content from PDF")
|
||||
st.markdown(combined_result)
|
||||
return True
|
||||
|
||||
|
||||
def process_youtube_or_audio(user_input):
|
||||
"""Process a YouTube URL or audio file and generate content."""
|
||||
if not generate_audio_blog(user_input):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def process_web_url(user_input):
|
||||
"""Process a web URL and generate content."""
|
||||
blog_from_url(user_input)
|
||||
return True
|
||||
|
||||
|
||||
def process_image_input(user_input, uploaded_file):
|
||||
"""Process an image file and generate content."""
|
||||
blog_from_image(user_input, uploaded_file)
|
||||
return True
|
||||
|
||||
|
||||
def handle_content_generation(input_type, user_input, uploaded_file, search_params, blog_params, selected_content_type):
|
||||
"""
|
||||
Handle content generation based on the input type.
|
||||
|
||||
Args:
|
||||
input_type: The type of input ("youtube_url", "web_url", etc.)
|
||||
user_input: The text input from the user
|
||||
uploaded_file: The uploaded file (if any)
|
||||
search_params: Search parameters
|
||||
blog_params: Blog content parameters
|
||||
selected_content_type: The selected content type
|
||||
|
||||
Returns:
|
||||
bool: True if content generation was successful, False otherwise
|
||||
"""
|
||||
# Create a status placeholder instead of a permanent message
|
||||
status_message = st.empty()
|
||||
status_message.info("Crafting your blog content... Please wait.")
|
||||
|
||||
try:
|
||||
if input_type == "keywords":
|
||||
result = process_keywords_input(user_input, search_params, blog_params, selected_content_type)
|
||||
# Clear the status message when done
|
||||
status_message.empty()
|
||||
return result
|
||||
|
||||
elif input_type == "youtube_url" or input_type == "audio_file":
|
||||
result = process_youtube_or_audio(user_input)
|
||||
status_message.empty()
|
||||
return result
|
||||
|
||||
elif input_type == "web_url":
|
||||
result = process_web_url(user_input)
|
||||
status_message.empty()
|
||||
return result
|
||||
|
||||
elif input_type == "image_file":
|
||||
result = process_image_input(user_input, uploaded_file)
|
||||
status_message.empty()
|
||||
return result
|
||||
|
||||
elif input_type == "PDF_file":
|
||||
result = process_pdf_input(uploaded_file)
|
||||
status_message.empty()
|
||||
return result
|
||||
|
||||
else:
|
||||
status_message.empty()
|
||||
st.error(f"Unsupported input type: {input_type}")
|
||||
return False
|
||||
except Exception as e:
|
||||
status_message.empty()
|
||||
st.error(f"An error occurred during content generation: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def generate_blog_content(search_keywords, google_search_result, tavily_search_result,
|
||||
google_search_success, tavily_search_success, blog_params, status):
|
||||
"""
|
||||
Generate blog content using either Google or Tavily search results.
|
||||
|
||||
Args:
|
||||
search_keywords (str): Search keywords
|
||||
google_search_result: Results from Google search
|
||||
tavily_search_result: Results from Tavily search
|
||||
google_search_success (bool): Whether Google search was successful
|
||||
tavily_search_success (bool): Whether Tavily search was successful
|
||||
blog_params (dict): Blog parameters
|
||||
status: Streamlit status object
|
||||
|
||||
Returns:
|
||||
str: Generated blog content or None if generation failed
|
||||
"""
|
||||
# Check if both searches failed - if so, stop the process
|
||||
if not google_search_success and not tavily_search_success:
|
||||
st.error("⛔ Both Google SERP and Tavily AI searches failed. Unable to generate blog content.")
|
||||
st.warning("Please check your API keys in the environment settings and try again.")
|
||||
return None
|
||||
|
||||
# Try Google results first if available
|
||||
if google_search_success and 'results' in google_search_result:
|
||||
try:
|
||||
status.update(label=f"✏️ Writing blog from Google Search results...")
|
||||
# Pass blog parameters to the blog writing function
|
||||
blog_style_info = f"""
|
||||
Length: {blog_params.get('blog_length')} words
|
||||
Tone: {blog_params.get('blog_tone')}
|
||||
Target Audience: {blog_params.get('blog_demographic')}
|
||||
Blog Type: {blog_params.get('blog_type')}
|
||||
Language: {blog_params.get('blog_language')}
|
||||
"""
|
||||
status.update(label=f"✏️ Writing {blog_params.get('blog_tone')} {blog_params.get('blog_type')} blog for {blog_params.get('blog_demographic')} audience...")
|
||||
blog_markdown_str = write_blog_google_serp(search_keywords, google_search_result['results'], blog_params=blog_params)
|
||||
status.update(label="✅ Generated content from Google search results", state="complete")
|
||||
return blog_markdown_str
|
||||
except Exception as err:
|
||||
status.update(label=f"❌ Failed to generate content from Google results: {str(err)}", state="error")
|
||||
st.error(f"Failed to generate content from Google results: {err}")
|
||||
logger.error(f"Failed to process Google search results: {err}")
|
||||
|
||||
# If Google failed or had no results, try Tavily
|
||||
if tavily_search_success and tavily_search_result:
|
||||
try:
|
||||
status.update(label=f"✏️ Writing blog from Tavily search results...")
|
||||
status.update(label=f"✏️ Writing {blog_params.get('blog_tone')} {blog_params.get('blog_type')} blog for {blog_params.get('blog_demographic')} audience...")
|
||||
blog_markdown_str = write_blog_google_serp(search_keywords, tavily_search_result, blog_params=blog_params)
|
||||
status.update(label="✅ Generated content from Tavily search results", state="complete")
|
||||
return blog_markdown_str
|
||||
except Exception as err:
|
||||
status.update(label=f"❌ Failed to generate content from Tavily results: {str(err)}", state="error")
|
||||
st.error(f"Failed to generate content from Tavily results: {err}")
|
||||
logger.error(f"Failed to process Tavily search results: {err}")
|
||||
|
||||
# If we still don't have content, show error
|
||||
st.error("⛔ Failed to generate any blog content from the research results.")
|
||||
return None
|
||||
|
||||
|
||||
def generate_blog_metadata(blog_markdown_str, search_keywords, status):
|
||||
"""
|
||||
Generate metadata for the blog content.
|
||||
|
||||
Args:
|
||||
blog_markdown_str (str): Blog content
|
||||
search_keywords (str): Original search keywords
|
||||
status: Streamlit status object
|
||||
|
||||
Returns:
|
||||
tuple: (blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug)
|
||||
"""
|
||||
status.update(label="🔍 Generating title, meta description, tags, categories, hashtags, and slug...")
|
||||
try:
|
||||
# Get all 6 metadata values from blog_metadata
|
||||
blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = asyncio.run(blog_metadata(blog_markdown_str))
|
||||
status.update(label="✅ Generated blog metadata successfully")
|
||||
return blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug
|
||||
except Exception as err:
|
||||
st.error(f"Failed to get blog metadata: {err}")
|
||||
logger.error(f"Failed to get blog metadata: {err}")
|
||||
status.update(label="❌ Failed to get blog metadata", state="error")
|
||||
return None, None, None, None, None, None
|
||||
|
||||
|
||||
def generate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, status, blog_tags=None):
|
||||
"""
|
||||
Generate a featured image for the blog.
|
||||
|
||||
Args:
|
||||
blog_title (str): Blog title
|
||||
blog_meta_desc (str): Blog meta description
|
||||
blog_markdown_str (str): Blog content
|
||||
status: Streamlit status object
|
||||
blog_tags (list, optional): Blog tags to use for image prompt enhancement
|
||||
|
||||
Returns:
|
||||
str: Path to the generated image or None if generation failed
|
||||
"""
|
||||
try:
|
||||
status.update(label="🖼️ Generating featured image for blog...")
|
||||
|
||||
# Create a better prompt for image generation
|
||||
if blog_title and blog_meta_desc:
|
||||
# If we have both title and description, use them
|
||||
text_to_image = f"{blog_title}: {blog_meta_desc}"
|
||||
elif blog_title:
|
||||
# If we only have title, use it
|
||||
text_to_image = blog_title
|
||||
elif blog_meta_desc:
|
||||
# If we only have description, use it
|
||||
text_to_image = blog_meta_desc
|
||||
else:
|
||||
# Fallback to first 200 chars of content
|
||||
text_to_image = blog_markdown_str[:200]
|
||||
|
||||
# Ensure the prompt is of reasonable length
|
||||
if len(text_to_image) > 300:
|
||||
text_to_image = text_to_image[:300]
|
||||
|
||||
# Log the prompt being used
|
||||
logger.info(f"Generating image with prompt: {text_to_image}")
|
||||
status.update(label=f"🖼️ Creating image with prompt: \"{text_to_image[:50]}...\"")
|
||||
|
||||
# Extract blog tags if available
|
||||
blog_tags_list = blog_tags if isinstance(blog_tags, list) else []
|
||||
|
||||
# Attempt image generation with all available parameters
|
||||
generated_image_filepath = generate_image(
|
||||
user_prompt=text_to_image,
|
||||
title=blog_title,
|
||||
description=blog_meta_desc,
|
||||
tags=blog_tags_list,
|
||||
content=blog_markdown_str[:2000] # Limit content length to avoid too large payloads
|
||||
)
|
||||
|
||||
# If first attempt failed, try with a simplified prompt
|
||||
if not generated_image_filepath:
|
||||
logger.warning("First image generation attempt failed, trying with simplified prompt")
|
||||
status.update(label="⚠️ First image attempt failed, trying again with simplified prompt...")
|
||||
|
||||
# Create a simpler prompt
|
||||
simplified_prompt = " ".join(text_to_image.split()[:10])
|
||||
generated_image_filepath = generate_image(
|
||||
user_prompt=simplified_prompt,
|
||||
title=blog_title,
|
||||
description=blog_meta_desc,
|
||||
tags=blog_tags_list,
|
||||
content=blog_markdown_str[:1000] # Use even shorter content for the retry
|
||||
)
|
||||
|
||||
if generated_image_filepath:
|
||||
status.update(label="✅ Successfully generated featured image")
|
||||
return generated_image_filepath
|
||||
else:
|
||||
status.update(label="❌ Image generation failed - no image created", state="error")
|
||||
return None
|
||||
|
||||
except Exception as err:
|
||||
st.warning(f"Failed in Image generation: {err}")
|
||||
logger.error(f"Failed in Image generation: {err}")
|
||||
status.update(label="❌ Image generation failed - no image created", state="error")
|
||||
return None
|
||||
|
||||
|
||||
def regenerate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags=None):
|
||||
"""
|
||||
Regenerate a blog image on demand.
|
||||
|
||||
Args:
|
||||
blog_title (str): Blog title
|
||||
blog_meta_desc (str): Blog meta description
|
||||
blog_markdown_str (str): Blog content
|
||||
blog_tags (list, optional): Blog tags to use for image prompt enhancement
|
||||
|
||||
Returns:
|
||||
str: Path to the generated image or None if generation failed
|
||||
"""
|
||||
with st.status("Regenerating image...", expanded=True) as status:
|
||||
try:
|
||||
# Use keywords from title or description
|
||||
if blog_title:
|
||||
keywords = " ".join(blog_title.split()[:6])
|
||||
prompt = f"Blog illustration for: {keywords}"
|
||||
elif blog_meta_desc:
|
||||
keywords = " ".join(blog_meta_desc.split()[:6])
|
||||
prompt = f"Blog illustration for: {keywords}"
|
||||
else:
|
||||
keywords = blog_markdown_str.split()[:50]
|
||||
prompt = f"Blog illustration based on: {' '.join(keywords[:6])}"
|
||||
|
||||
status.update(label=f"🖼️ Generating new image with prompt: \"{prompt}\"")
|
||||
|
||||
# Extract any tags if available - will be passed as empty list otherwise
|
||||
blog_tags_list = blog_tags if isinstance(blog_tags, list) else []
|
||||
|
||||
# Generate the image with all parameters
|
||||
generated_image_filepath = generate_image(
|
||||
user_prompt=prompt,
|
||||
title=blog_title,
|
||||
description=blog_meta_desc,
|
||||
tags=blog_tags_list,
|
||||
content=blog_markdown_str[:2000] # Limit content length to avoid too large payloads
|
||||
)
|
||||
|
||||
if generated_image_filepath:
|
||||
status.update(label="✅ Successfully generated new image", state="complete")
|
||||
return generated_image_filepath
|
||||
else:
|
||||
status.update(label="❌ Image regeneration failed", state="error")
|
||||
return None
|
||||
|
||||
except Exception as err:
|
||||
st.error(f"Failed to regenerate image: {err}")
|
||||
logger.error(f"Image regeneration error: {err}")
|
||||
status.update(label="❌ Image regeneration failed", state="error")
|
||||
return None
|
||||
@@ -1,420 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
import streamlit as st
|
||||
from loguru import logger
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv(Path('../../../.env'))
|
||||
|
||||
# Import necessary modules
|
||||
from ...ai_web_researcher.gpt_online_researcher import (
|
||||
do_google_serp_search as gpt_do_google_serp_search,
|
||||
do_tavily_ai_search as gpt_do_tavily_ai_search
|
||||
)
|
||||
from ...ai_web_researcher.tavily_ai_search import do_tavily_ai_search as tavily_direct_search
|
||||
|
||||
|
||||
def initialize_parameters(search_params=None, blog_params=None):
|
||||
"""
|
||||
Initialize and validate search and blog parameters with defaults.
|
||||
|
||||
Args:
|
||||
search_params (dict, optional): Search parameters
|
||||
blog_params (dict, optional): Blog parameters
|
||||
|
||||
Returns:
|
||||
tuple: (search_params, blog_params) with defaults applied
|
||||
"""
|
||||
# Initialize search params if not provided
|
||||
if search_params is None:
|
||||
search_params = {}
|
||||
|
||||
# Initialize blog params if not provided
|
||||
if blog_params is None:
|
||||
blog_params = {}
|
||||
|
||||
# Provide default values only for missing keys
|
||||
# This ensures we don't override values that were intentionally set to 0 or other falsy values
|
||||
if "max_results" not in search_params:
|
||||
search_params["max_results"] = 10
|
||||
if "search_depth" not in search_params:
|
||||
search_params["search_depth"] = "basic"
|
||||
if "time_range" not in search_params:
|
||||
search_params["time_range"] = "year"
|
||||
if "include_domains" not in search_params:
|
||||
search_params["include_domains"] = []
|
||||
|
||||
# Provide default values only for missing blog parameter keys
|
||||
if "blog_length" not in blog_params:
|
||||
blog_params["blog_length"] = 2000
|
||||
if "blog_tone" not in blog_params:
|
||||
blog_params["blog_tone"] = "Professional"
|
||||
if "blog_demographic" not in blog_params:
|
||||
blog_params["blog_demographic"] = "Professional"
|
||||
if "blog_type" not in blog_params:
|
||||
blog_params["blog_type"] = "Informational"
|
||||
if "blog_language" not in blog_params:
|
||||
blog_params["blog_language"] = "English"
|
||||
if "blog_output_format" not in blog_params:
|
||||
blog_params["blog_output_format"] = "markdown"
|
||||
|
||||
# Log the parameters for debugging
|
||||
logger.info(f"Using search parameters: {search_params}")
|
||||
logger.info(f"Using blog parameters: {blog_params}")
|
||||
|
||||
return search_params, blog_params
|
||||
|
||||
|
||||
def perform_google_search(search_keywords, search_params, status, status_container, progress_bar):
|
||||
"""
|
||||
Perform Google SERP search for the given keywords.
|
||||
|
||||
Args:
|
||||
search_keywords (str): Keywords to search for
|
||||
search_params (dict): Search parameters
|
||||
status: Streamlit status object
|
||||
status_container: Streamlit container for status messages
|
||||
progress_bar: Streamlit progress bar
|
||||
|
||||
Returns:
|
||||
tuple: (google_search_result, g_titles, success_flag)
|
||||
"""
|
||||
def update_progress(message, progress=None, level="info"):
|
||||
"""Helper function to update progress in Streamlit UI"""
|
||||
if progress is not None:
|
||||
progress_bar.progress(progress)
|
||||
|
||||
if level == "error":
|
||||
status_container.error(f"🚫 {message}")
|
||||
elif level == "warning":
|
||||
status_container.warning(f"⚠️ {message}")
|
||||
elif level == "success":
|
||||
status_container.success(f"✅ {message}")
|
||||
else:
|
||||
status_container.info(f"🔄 {message}")
|
||||
logger.debug(f"Progress update [{level}]: {message}")
|
||||
|
||||
try:
|
||||
# Update the function call to include the required parameters and search_params
|
||||
status.update(label=f"Starting Google SERP search for: {search_keywords}")
|
||||
|
||||
# Add search params to the Google SERP search
|
||||
google_search_params = {
|
||||
"max_results": search_params.get("max_results", 10)
|
||||
}
|
||||
|
||||
# Include domains if provided
|
||||
if search_params.get("include_domains"):
|
||||
google_search_params["include_domains"] = search_params.get("include_domains")
|
||||
|
||||
google_search_result = do_google_serp_search(
|
||||
search_keywords,
|
||||
status_container=status_container,
|
||||
update_progress=update_progress,
|
||||
**google_search_params
|
||||
)
|
||||
|
||||
if google_search_result and google_search_result.get('titles') and len(google_search_result.get('titles', [])) > 0:
|
||||
status.update(label=f"✅ Finished with Google web for Search: {search_keywords}")
|
||||
g_titles = google_search_result.get('titles', [])
|
||||
return google_search_result, g_titles, True
|
||||
else:
|
||||
# Check if there's an error message in the result
|
||||
if google_search_result and 'summary' in google_search_result and 'Error' in google_search_result['summary']:
|
||||
error_msg = google_search_result['summary']
|
||||
status.update(label=f"❌ Google search failed: {error_msg}", state="error")
|
||||
st.error(f"Google SERP search failed: {error_msg}")
|
||||
else:
|
||||
status.update(label="❌ Failed to get Google SERP results. No valid data returned.", state="error")
|
||||
st.error("Google SERP search failed to return valid results.")
|
||||
return google_search_result, [], False
|
||||
except Exception as err:
|
||||
status.update(label=f"❌ Google search error: {str(err)}", state="error")
|
||||
st.error(f"Google web research failed: {err}")
|
||||
logger.error(f"Failed in Google web research: {err}")
|
||||
return None, [], False
|
||||
|
||||
|
||||
def perform_tavily_search(search_keywords, search_params, status):
|
||||
"""
|
||||
Perform Tavily AI search for the given keywords.
|
||||
|
||||
Args:
|
||||
search_keywords (str): Keywords to search for
|
||||
search_params (dict): Search parameters
|
||||
status: Streamlit status object
|
||||
|
||||
Returns:
|
||||
tuple: (tavily_search_result, success_flag)
|
||||
"""
|
||||
try:
|
||||
status.update(label=f"🔍 Starting Tavily AI research: {search_keywords}")
|
||||
|
||||
# Pass the search parameters to Tavily
|
||||
tavily_result_tuple = do_tavily_ai_search(
|
||||
search_keywords,
|
||||
max_results=search_params.get("max_results", 10),
|
||||
search_depth=search_params.get("search_depth", "basic"),
|
||||
include_domains=search_params.get("include_domains", []),
|
||||
time_range=search_params.get("time_range", "year")
|
||||
)
|
||||
|
||||
if tavily_result_tuple and len(tavily_result_tuple) == 3:
|
||||
tavily_search_result, t_titles, t_answer = tavily_result_tuple
|
||||
# If we have either titles or an answer, consider it a success
|
||||
if (t_titles and len(t_titles) > 0) or (t_answer and len(t_answer) > 10):
|
||||
status.update(label=f"✅ Finished Tavily AI Search on: {search_keywords}", state="complete")
|
||||
return tavily_search_result, True
|
||||
else:
|
||||
status.update(label="❌ Tavily search returned empty results", state="error")
|
||||
st.warning("Tavily search didn't find relevant information.")
|
||||
return tavily_search_result, False
|
||||
else:
|
||||
status.update(label="❌ Tavily search returned incomplete results", state="error")
|
||||
st.error("Tavily search failed to return valid results.")
|
||||
return None, False
|
||||
|
||||
except Exception as err:
|
||||
status.update(label=f"❌ Tavily search error: {str(err)}", state="error")
|
||||
st.error(f"Failed in Tavily web research: {err}")
|
||||
logger.error(f"Failed in Tavily web research: {err}")
|
||||
return None, False
|
||||
|
||||
|
||||
def do_google_serp_search(search_keywords, status_container=None, update_progress=None, **kwargs):
|
||||
"""
|
||||
Wrapper function to handle the parameter mismatch with the original function.
|
||||
"""
|
||||
try:
|
||||
if status_container is None:
|
||||
status_container = st.empty()
|
||||
|
||||
if update_progress is None:
|
||||
def update_progress(message, progress=None, level="info"):
|
||||
if level == "error":
|
||||
status_container.error(message)
|
||||
elif level == "warning":
|
||||
status_container.warning(message)
|
||||
else:
|
||||
status_container.info(message)
|
||||
|
||||
# Create a fixed update_progress function that handles any progress type
|
||||
def safe_update_progress(message, progress=None, level="info"):
|
||||
try:
|
||||
# Handle progress value of different types
|
||||
if progress is not None:
|
||||
if isinstance(progress, str):
|
||||
# Try to convert string to float if it represents a number
|
||||
try:
|
||||
progress = float(progress)
|
||||
except ValueError:
|
||||
# If conversion fails, just log the message without updating progress
|
||||
progress = None
|
||||
|
||||
# Call the original update_progress with sanitized values
|
||||
update_progress(message, progress, level)
|
||||
except Exception as err:
|
||||
# If there's an error in the progress function, just log to console
|
||||
logger.error(f"Error in progress update: {err}")
|
||||
# Try one more time with just the message
|
||||
try:
|
||||
update_progress(message, None, level)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Set default search parameters - fix the parameter to use 'max_results' not 'num_results'
|
||||
search_params = {
|
||||
"max_results": kwargs.get("max_results", 10),
|
||||
"include_domains": kwargs.get("include_domains", []),
|
||||
"search_depth": kwargs.get("search_depth", "basic")
|
||||
}
|
||||
|
||||
# Update status to indicate we're checking API keys
|
||||
status_container.info("🔑 Checking required API keys...")
|
||||
|
||||
# Call the original function with the required parameters
|
||||
result = gpt_do_google_serp_search(search_keywords, status_container, safe_update_progress, **search_params)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"Error in do_google_serp_search wrapper: {error_msg}")
|
||||
|
||||
# Check for common error patterns and display user-friendly messages
|
||||
if "SERPER_API_KEY is missing" in error_msg:
|
||||
status_container.error("🔑 Google search API key (SERPER_API_KEY) is missing. Please check your environment settings.")
|
||||
st.error("Google SERP search failed: API key is missing. Using alternative methods.")
|
||||
elif "Progress Value has invalid type" in error_msg:
|
||||
# This is an internal error, log it but show a more user-friendly message
|
||||
status_container.warning("⚠️ Internal progress tracking error. Continuing with search.")
|
||||
else:
|
||||
# For unknown errors, show the full error message
|
||||
status_container.error(f"🚫 Google search error: {error_msg}")
|
||||
st.error(f"Google SERP search failed: {error_msg}")
|
||||
|
||||
# Return a minimal result structure to prevent downstream errors
|
||||
return {
|
||||
'results': {},
|
||||
'titles': [],
|
||||
'summary': f"Error occurred during search: {error_msg}",
|
||||
'stats': {
|
||||
'organic_count': 0,
|
||||
'questions_count': 0,
|
||||
'related_count': 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def do_tavily_ai_search(keywords, max_results=10, search_depth="basic", include_domains=None, time_range="year"):
|
||||
"""
|
||||
Wrapper function for Tavily search to handle parameter differences.
|
||||
|
||||
Args:
|
||||
keywords (str): Keywords to search for
|
||||
max_results (int): Maximum number of search results to return
|
||||
search_depth (str): "basic" or "advanced" search depth
|
||||
include_domains (list): List of domains to prioritize in search
|
||||
time_range (str): Time range for results ("day", "week", "month", "year", "all")
|
||||
"""
|
||||
status_container = st.empty()
|
||||
|
||||
if include_domains is None:
|
||||
include_domains = []
|
||||
|
||||
try:
|
||||
# Show status message
|
||||
status_container.info(f"🔍 Preparing Tavily AI search with {search_depth} depth...")
|
||||
|
||||
# FIXED: Ensure all parameters have correct types to prevent comparison errors
|
||||
tavily_params = {
|
||||
'max_results': int(max_results), # Explicitly convert to int
|
||||
'search_depth': str(search_depth), # Ensure this is a string
|
||||
'include_domains': include_domains,
|
||||
'time_range': str(time_range)
|
||||
}
|
||||
|
||||
# Log the parameters for debugging
|
||||
logger.info(f"Tavily search parameters: {tavily_params}")
|
||||
|
||||
# Check for API key before making the request
|
||||
tavily_api_key = os.environ.get("TAVILY_API_KEY")
|
||||
if not tavily_api_key:
|
||||
status_container.error("🔑 Tavily API key (TAVILY_API_KEY) is missing. Please check your environment settings.")
|
||||
st.error("Tavily search failed: API key is missing. Using alternative methods.")
|
||||
return None, [], "API key missing"
|
||||
|
||||
status_container.info(f"🔍 Searching with Tavily AI using {search_depth} depth for: {keywords}")
|
||||
|
||||
# Direct implementation without calling gpt_do_tavily_ai_search to avoid type issues
|
||||
try:
|
||||
# Call the function directly with correct parameter types
|
||||
tavily_raw_results = tavily_direct_search(
|
||||
keywords,
|
||||
max_results=tavily_params['max_results'],
|
||||
search_depth=tavily_params['search_depth'],
|
||||
include_domains=tavily_params['include_domains'],
|
||||
time_range=tavily_params['time_range']
|
||||
)
|
||||
|
||||
# Extract the needed information
|
||||
if isinstance(tavily_raw_results, tuple) and len(tavily_raw_results) == 3:
|
||||
# If already in the right format, use it directly
|
||||
return tavily_raw_results
|
||||
|
||||
# Process the results to extract titles and answer
|
||||
t_results = tavily_raw_results
|
||||
t_titles = []
|
||||
t_answer = ""
|
||||
|
||||
# Extract titles from results if available
|
||||
if isinstance(t_results, dict):
|
||||
if 'results' in t_results and isinstance(t_results['results'], list):
|
||||
t_titles = [r.get('title', '') for r in t_results['results']]
|
||||
status_container.success(f"✅ Found {len(t_titles)} relevant articles")
|
||||
if 'answer' in t_results:
|
||||
t_answer = t_results['answer']
|
||||
status_container.success("✅ Generated a summary answer")
|
||||
|
||||
return t_results, t_titles, t_answer
|
||||
|
||||
except ImportError:
|
||||
# Fall back to the original function if direct import fails
|
||||
status_container.warning("⚠️ Using fallback Tavily search method...")
|
||||
logger.warning("Using fallback Tavily search method")
|
||||
|
||||
# FIXED: Alternative approach - wrap the call in try/except to handle type errors
|
||||
try:
|
||||
tavily_result = gpt_do_tavily_ai_search(keywords, **tavily_params)
|
||||
|
||||
# Format the result to match what the blog writer expects
|
||||
if isinstance(tavily_result, tuple) and len(tavily_result) == 3:
|
||||
status_container.success("✅ Tavily search completed successfully")
|
||||
return tavily_result
|
||||
|
||||
# If not a tuple with expected values, try to extract what we need
|
||||
t_results = tavily_result
|
||||
|
||||
# Extract titles and answer if available
|
||||
t_titles = []
|
||||
t_answer = ""
|
||||
|
||||
if isinstance(t_results, dict):
|
||||
if 'results' in t_results and isinstance(t_results['results'], list):
|
||||
t_titles = [r.get('title', '') for r in t_results['results']]
|
||||
status_container.success(f"✅ Found {len(t_titles)} relevant articles")
|
||||
if 'answer' in t_results:
|
||||
t_answer = t_results['answer']
|
||||
status_container.success("✅ Generated a summary answer")
|
||||
|
||||
return t_results, t_titles, t_answer
|
||||
|
||||
except TypeError as type_err:
|
||||
# Handle the specific type error more gracefully
|
||||
error_msg = str(type_err)
|
||||
logger.error(f"Type error in Tavily search: {error_msg}")
|
||||
|
||||
if "'>' not supported" in error_msg:
|
||||
status_container.error("🚫 Tavily search parameter type error. Trying alternative approach...")
|
||||
|
||||
# Try a simpler approach with minimal parameters
|
||||
try:
|
||||
# Call with only the keyword and fixed max_results
|
||||
tavily_result = gpt_do_tavily_ai_search(keywords, max_results=10)
|
||||
|
||||
# Minimal processing to extract titles and answer
|
||||
t_results = tavily_result
|
||||
t_titles = []
|
||||
t_answer = ""
|
||||
|
||||
if isinstance(t_results, dict):
|
||||
if 'results' in t_results and isinstance(t_results['results'], list):
|
||||
t_titles = [r.get('title', '') for r in t_results['results']]
|
||||
if 'answer' in t_results:
|
||||
t_answer = t_results['answer']
|
||||
|
||||
return t_results, t_titles, t_answer
|
||||
except Exception as inner_err:
|
||||
logger.error(f"Alternative Tavily approach also failed: {inner_err}")
|
||||
raise
|
||||
else:
|
||||
# Re-raise other type errors
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"Error in do_tavily_ai_search wrapper: {error_msg}")
|
||||
|
||||
# Display user-friendly error message
|
||||
status_container.error(f"🚫 Tavily search error: {error_msg}")
|
||||
st.error(f"Tavily AI search failed: {error_msg}")
|
||||
|
||||
# Return empty results to prevent downstream errors
|
||||
return None, [], f"Error: {error_msg}"
|
||||
|
||||
finally:
|
||||
# Clear the status container after a delay
|
||||
time.sleep(2)
|
||||
status_container.empty()
|
||||
@@ -1,199 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
|
||||
from ...gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
def write_blog_google_serp(keywords, search_results, blog_params=None):
|
||||
"""
|
||||
Write a blog post using search results from Google SERP.
|
||||
|
||||
Args:
|
||||
keywords (str): The keywords or topic for the blog
|
||||
search_results (dict): Results from Google SERP search
|
||||
blog_params (dict, optional): Blog content characteristics:
|
||||
- blog_length: Target word count
|
||||
- blog_tone: Content tone
|
||||
- blog_demographic: Target audience
|
||||
- blog_type: Type of blog post
|
||||
- blog_language: Language for the blog
|
||||
|
||||
Returns:
|
||||
str: The generated blog content in markdown format
|
||||
"""
|
||||
# If no blog parameters are provided, use defaults
|
||||
if blog_params is None:
|
||||
blog_params = {
|
||||
"blog_length": 2000,
|
||||
"blog_tone": "Professional",
|
||||
"blog_demographic": "Professional",
|
||||
"blog_type": "Informational",
|
||||
"blog_language": "English"
|
||||
}
|
||||
|
||||
# Ensure all parameters have default values
|
||||
blog_length = blog_params.get("blog_length", 2000)
|
||||
blog_tone = blog_params.get("blog_tone", "Professional")
|
||||
blog_demographic = blog_params.get("blog_demographic", "Professional")
|
||||
blog_type = blog_params.get("blog_type", "Informational")
|
||||
blog_language = blog_params.get("blog_language", "English")
|
||||
|
||||
logger.info(f"Generating {blog_tone} {blog_type} blog of {blog_length} words for {blog_demographic} audience in {blog_language}")
|
||||
|
||||
try:
|
||||
# Build a prompt based on search results
|
||||
prompt_parts = [
|
||||
f"You are a specialized blog writer who writes in a {blog_tone} tone for a {blog_demographic} audience. "
|
||||
f"Create a {blog_type} blog post that is approximately {blog_length} words in {blog_language}.",
|
||||
f"The blog should be about: {keywords}",
|
||||
"Use the following search results to create an informative, accurate, and well-structured blog post:"
|
||||
]
|
||||
|
||||
# Add organic search results
|
||||
if 'organic' in search_results:
|
||||
prompt_parts.append("\nSearch results:")
|
||||
for i, result in enumerate(search_results['organic'][:5], 1):
|
||||
title = result.get('title', 'No title')
|
||||
snippet = result.get('snippet', 'No snippet')
|
||||
prompt_parts.append(f"{i}. {title}: {snippet}")
|
||||
|
||||
# Add people also ask questions if available
|
||||
if 'peopleAlsoAsk' in search_results and search_results['peopleAlsoAsk']:
|
||||
prompt_parts.append("\nPeople also ask:")
|
||||
for i, question in enumerate(search_results['peopleAlsoAsk'][:3], 1):
|
||||
q_text = question.get('question', 'No question')
|
||||
q_answer = question.get('answer', {}).get('snippet', 'No answer')
|
||||
prompt_parts.append(f"{i}. Q: {q_text}\n A: {q_answer}")
|
||||
|
||||
# Add related searches if available
|
||||
if 'relatedSearches' in search_results and search_results['relatedSearches']:
|
||||
related = [item.get('query', '') for item in search_results['relatedSearches'][:5]]
|
||||
if related:
|
||||
prompt_parts.append("\nRelated topics to consider including:")
|
||||
prompt_parts.append(", ".join(related))
|
||||
|
||||
# Add specific instructions based on blog_type
|
||||
type_instructions = {
|
||||
"Informational": "Focus on providing factual information and educating the reader about the topic.",
|
||||
"How-to": "Include clear step-by-step instructions with actionable advice.",
|
||||
"List": "Organize content into a numbered or bulleted list of points, tips, or examples.",
|
||||
"Review": "Provide balanced analysis with pros and cons, and a clear conclusion or recommendation.",
|
||||
"Tutorial": "Include detailed instructions with examples and explanations for each step.",
|
||||
"Opinion": "Present a clear perspective supported by evidence, while acknowledging other viewpoints."
|
||||
}
|
||||
|
||||
prompt_parts.append(f"\nSpecific instructions: {type_instructions.get(blog_type, '')}")
|
||||
|
||||
# Add formatting instructions
|
||||
prompt_parts.append("""
|
||||
Format the blog post in markdown with:
|
||||
- A compelling title (# Title)
|
||||
- An introduction that hooks the reader
|
||||
- Well-structured sections with appropriate headings (## Headings)
|
||||
- Bullet points or numbered lists where appropriate
|
||||
- A conclusion summarizing key points
|
||||
- Make sure all content is accurate, informative, and adds value to the reader.
|
||||
- Include 2-3 subheadings to organize the content well.
|
||||
- Be concise and to the point.
|
||||
- Write in an engaging, reader-friendly style.
|
||||
- Avoid using phrases like "According to the search results" or "Based on the information provided."
|
||||
- Present information as direct knowledge.
|
||||
""")
|
||||
|
||||
# Combine all prompt parts
|
||||
full_prompt = "\n".join(prompt_parts)
|
||||
|
||||
# Generate the blog content using the prompt
|
||||
response = llm_text_gen(full_prompt)
|
||||
|
||||
# Return the generated content
|
||||
return response
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"Error generating blog from search results: {err}")
|
||||
raise
|
||||
|
||||
|
||||
def improve_blog_intro(blog_content, blog_intro):
|
||||
"""Combine the given online research and gpt blog content"""
|
||||
prompt = f"""
|
||||
You are a skilled content editor, tasked with creating an engaging peek into the blog post provided.
|
||||
This peek should entice readers to delve into the full content.
|
||||
|
||||
Here's what you need to do:
|
||||
1. **Replace the old blog introduction with the new one provided.**
|
||||
2. **Craft a short and captivating summary of the key points and interesting takeaways from the blog.**
|
||||
- Highlight what makes the blog unique and worth reading.
|
||||
- This peek should be placed directly before the new introduction.
|
||||
3. **Include the complete blog content, with the new introduction and the added peek.**
|
||||
|
||||
Do not provide explanations for your actions, simply present the edited blog content.
|
||||
|
||||
Blog Content: \"\"\"{blog_content}\"\"\"
|
||||
Blog Introduction: \"\"\"{blog_intro}\"\"\"
|
||||
"""
|
||||
logger.info("Generating blog introduction from tavily answer.")
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Exit: Failed to get response from LLM: {err}")
|
||||
exit(1)
|
||||
|
||||
|
||||
def blog_with_keywords(blog, keywords):
|
||||
"""Combine the given online research and gpt blog content"""
|
||||
prompt = f"""
|
||||
You are Sarah, the Creative Content writer, writing up fresh ideas and crafts them with care.
|
||||
She makes complex topics easy to understand and writes in a friendly tone that connects with everyone.
|
||||
She excels at simplifying complex topics and communicates with charisma, making technical jargon come alive for her audience.
|
||||
|
||||
As an expert digital content writer, specializing in content optimization and SEO.
|
||||
I will provide you with my 'blog content' and 'list of keywords' on the same topic.
|
||||
Your task is to write an original blog, utilizing given keywords and blog content.
|
||||
Your blog should be highly detailed and well formatted.
|
||||
|
||||
Blog content: '{blog}'
|
||||
list of keywords: '{keywords}'
|
||||
"""
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"blog_with_keywords: Failed to get response from LLM: {err}")
|
||||
raise err
|
||||
|
||||
|
||||
def blog_with_research(report, blog):
|
||||
"""Combine the given online research and gpt blog content"""
|
||||
prompt = f"""
|
||||
As expert Creative Content writer, Your task is to update a blog post using the latest research.
|
||||
|
||||
Here's what you need to do:
|
||||
|
||||
1. **Read the outdated blog content and the new research report carefully.**
|
||||
2. **Identify key insights and updates from the research report that should be incorporated into the blog post.**
|
||||
3. **Rewrite sections of the blog post to reflect the new information, ensuring a smooth and natural flow.**
|
||||
4. **Maintain the blog's original friendly and conversational tone throughout.**
|
||||
|
||||
Remember, your goal is to seamlessly blend the new information into the existing blog post, making it accurate and engaging for readers.
|
||||
\n\n
|
||||
Research Report: \"\"\"{report}\"\"\"
|
||||
|
||||
Blog Content: \"\"\"{blog}\"\"\"
|
||||
"""
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"blog_with_research: Failed to get response from LLM: {err}")
|
||||
raise err
|
||||
@@ -1,252 +0,0 @@
|
||||
import streamlit as st
|
||||
|
||||
def apply_blog_writer_styles():
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* Base UI improvements */
|
||||
body, .main .block-container {
|
||||
background: linear-gradient(135deg, #f0f4f8 0%, #d7e1ec 100%) !important;
|
||||
min-height: 100vh;
|
||||
color: #2c3e50;
|
||||
font-family: 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
/* Main layout improvements */
|
||||
.main .block-container {
|
||||
padding: 1rem 2rem 2rem 2rem !important;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Back button styling */
|
||||
[data-testid="stButton"] > button:first-of-type {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 10px rgba(25, 118, 210, 0.2);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
[data-testid="stButton"] > button:first-of-type:hover {
|
||||
background: #1565c0;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 15px rgba(25, 118, 210, 0.3);
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.blog-header, .page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
padding: 2rem 1.5rem;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f5f7fa 100%);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.blog-header h1, .page-header h1 {
|
||||
font-size: 2.5em;
|
||||
font-family: 'Helvetica Neue', sans-serif;
|
||||
font-weight: 700;
|
||||
color: #1976d2;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.blog-header p, .page-header p {
|
||||
font-size: 1.1em;
|
||||
color: #546e7a;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Input section styling */
|
||||
.stTextArea textarea, .stTextInput input {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #2c3e50;
|
||||
font-size: 1rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.stTextArea textarea:focus, .stTextInput input:focus {
|
||||
border: 1.5px solid #1976d2;
|
||||
box-shadow: 0 2px 10px rgba(25, 118, 210, 0.12);
|
||||
}
|
||||
|
||||
/* File uploader styling */
|
||||
.stFileUploader > div {
|
||||
background: #ffffff;
|
||||
border: 2px dashed #cfd8dc;
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem 1rem;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.stFileUploader > div:hover {
|
||||
border-color: #1976d2;
|
||||
background: rgba(25, 118, 210, 0.03);
|
||||
}
|
||||
|
||||
/* Options expander styling */
|
||||
.stExpander {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin: 1.5rem 0;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.stExpander > details {
|
||||
background: #ffffff;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.stExpander > details > summary {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.stExpander > details > summary:hover {
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
/* Checkbox styling */
|
||||
.stCheckbox > div {
|
||||
background: #ffffff;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.stCheckbox > div:hover {
|
||||
background: rgba(25, 118, 210, 0.03);
|
||||
}
|
||||
|
||||
.stCheckbox label {
|
||||
font-weight: 500;
|
||||
color: #455a64;
|
||||
}
|
||||
|
||||
/* Radio button styling */
|
||||
.stRadio > div {
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.stRadio > div > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stRadio > div > div > label {
|
||||
background: #f5f7fa;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border-radius: 30px;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
color: #546e7a;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.stRadio > div > div > label:hover {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
border-color: #bbdefb;
|
||||
}
|
||||
|
||||
.stRadio > div > div > label[data-baseweb="radio"] input:checked + div {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
/* Generate button styling */
|
||||
button[data-testid="baseButton-secondary"],
|
||||
button[data-testid="baseButton-primary"] {
|
||||
background: linear-gradient(45deg, #1976d2, #2196f3);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
padding: 0.85rem 1.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(25, 118, 210, 0.25);
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
button[data-testid="baseButton-secondary"]:hover,
|
||||
button[data-testid="baseButton-primary"]:hover {
|
||||
background: linear-gradient(45deg, #1565c0, #1976d2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(25, 118, 210, 0.35);
|
||||
}
|
||||
|
||||
/* Input labels */
|
||||
.stTextArea label, .stTextInput label, .stFileUploader label {
|
||||
font-weight: 600;
|
||||
color: #455a64;
|
||||
font-size: 1.05rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Section headers */
|
||||
.stMarkdown h3 {
|
||||
color: #1976d2;
|
||||
font-weight: 600;
|
||||
font-size: 1.3rem;
|
||||
margin: 1.5rem 0 0.75rem 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid rgba(25, 118, 210, 0.1);
|
||||
}
|
||||
|
||||
.stMarkdown h4 {
|
||||
color: #455a64;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
margin: 1rem 0 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Column layout improvements */
|
||||
[data-testid="column"] {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 1.2rem;
|
||||
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
/* Success and error messages */
|
||||
.stSuccess, .stInfo, .stError {
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
@@ -1,864 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
from textwrap import dedent
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import streamlit as st
|
||||
from gtts import gTTS
|
||||
import base64
|
||||
from dotenv import load_dotenv
|
||||
import time
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv(Path('../../.env'))
|
||||
# Logger setup
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}")
|
||||
|
||||
# Import other necessary modules
|
||||
from ...ai_web_researcher.gpt_online_researcher import (
|
||||
do_metaphor_ai_research, do_google_pytrends_analysis)
|
||||
from .blog_from_google_serp import write_blog_google_serp, blog_with_research
|
||||
from ...blog_metadata.get_blog_metadata import blog_metadata
|
||||
from ...blog_postprocessing.save_blog_to_file import save_blog_to_file
|
||||
from ...gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
||||
from ...ai_seo_tools.content_title_generator import generate_blog_titles
|
||||
from ...ai_seo_tools.meta_desc_generator import generate_blog_metadesc
|
||||
from ...ai_seo_tools.seo_structured_data import ai_structured_data
|
||||
|
||||
# Import search functions from the research utils module
|
||||
from .blog_ai_research_utils import (
|
||||
initialize_parameters,
|
||||
perform_google_search,
|
||||
perform_tavily_search,
|
||||
do_google_serp_search,
|
||||
do_tavily_ai_search
|
||||
)
|
||||
|
||||
|
||||
def save_blog_content(blog_markdown_str, blog_title, blog_meta_desc, blog_tags, blog_categories, generated_image_filepath, status, blog_hashtags=None, blog_slug=None):
|
||||
"""
|
||||
Save the blog content to a file.
|
||||
|
||||
Args:
|
||||
blog_markdown_str (str): Blog content
|
||||
blog_title (str): Blog title
|
||||
blog_meta_desc (str): Blog meta description
|
||||
blog_tags (list): Blog tags
|
||||
blog_categories (list): Blog categories
|
||||
generated_image_filepath (str): Path to the generated image
|
||||
status: Streamlit status object
|
||||
blog_hashtags (str, optional): Social media hashtags
|
||||
blog_slug (str, optional): SEO-friendly URL slug
|
||||
|
||||
Returns:
|
||||
str: Path to the saved file or None if saving failed
|
||||
"""
|
||||
try:
|
||||
status.update(label="💾 Saving blog content to file...")
|
||||
saved_blog_to_file = save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc,
|
||||
blog_tags, blog_categories, generated_image_filepath)
|
||||
status.update(label=f"✅ Saved the content to: {saved_blog_to_file}")
|
||||
return saved_blog_to_file
|
||||
except Exception as err:
|
||||
st.error(f"Failed to save blog to file: {err}")
|
||||
logger.error(f"Failed to save blog to file: {err}")
|
||||
status.update(label="❌ Failed to save blog to file", state="error")
|
||||
return None
|
||||
|
||||
|
||||
def generate_audio_version(blog_markdown_str, status=None):
|
||||
"""
|
||||
Generate an audio version of the blog content.
|
||||
|
||||
Args:
|
||||
blog_markdown_str (str): Blog content
|
||||
status: Streamlit status object (optional)
|
||||
|
||||
Returns:
|
||||
bool: True if audio generation was successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
if status:
|
||||
status.update(label="🔊 Generating audio version of the blog...")
|
||||
else:
|
||||
st.info("🔊 Generating audio version...")
|
||||
|
||||
# Only generate audio for reasonable-sized blogs (to avoid errors with very large text)
|
||||
if blog_markdown_str and len(blog_markdown_str) < 50000: # Max ~50KB of text
|
||||
tts = gTTS(text=blog_markdown_str[:40000], lang='en', slow=False) # Use first 40K chars to be safe
|
||||
tts.save("delete_me.mp3")
|
||||
st.audio("delete_me.mp3")
|
||||
st.download_button(
|
||||
label="📥 Download Audio File",
|
||||
data=open("delete_me.mp3", "rb").read(),
|
||||
file_name="blog_audio.mp3",
|
||||
mime="audio/mp3"
|
||||
)
|
||||
if status:
|
||||
status.update(label="✅ Audio version generated successfully", state="complete")
|
||||
else:
|
||||
st.success("✅ Audio version generated successfully")
|
||||
return True
|
||||
else:
|
||||
st.warning("Blog content too large for audio generation")
|
||||
if status:
|
||||
status.update(label="⚠️ Blog content too large for audio generation", state="complete")
|
||||
return False
|
||||
except Exception as err:
|
||||
st.warning(f"Failed to generate audio version: {err}")
|
||||
logger.error(f"Failed to generate audio version: {err}")
|
||||
if status:
|
||||
status.update(label="❌ Failed to generate audio version", state="error")
|
||||
return False
|
||||
|
||||
|
||||
# Helper functions for write_blog_from_keywords
|
||||
def setup_progress_tracking():
|
||||
"""Set up progress tracking elements for blog generation."""
|
||||
# Create a placeholder for the final blog content
|
||||
final_content_placeholder = st.empty()
|
||||
|
||||
# Create progress tracking
|
||||
progress_placeholder = st.empty()
|
||||
with progress_placeholder.container():
|
||||
progress_bar = st.progress(0)
|
||||
status_text = st.empty()
|
||||
|
||||
def update_progress(step, total_steps, message):
|
||||
"""Update the progress bar and status message"""
|
||||
progress_value = min(step / total_steps, 1.0)
|
||||
progress_bar.progress(progress_value)
|
||||
status_text.info(f"Step {step}/{total_steps}: {message}")
|
||||
|
||||
# When process is complete, clear the progress info
|
||||
if step == total_steps:
|
||||
import time
|
||||
time.sleep(3) # Show the complete message for 3 seconds
|
||||
progress_bar.empty()
|
||||
status_text.empty()
|
||||
|
||||
return final_content_placeholder, progress_placeholder, progress_bar, status_text, update_progress
|
||||
|
||||
|
||||
def perform_research_phase(search_keywords, search_params, update_progress):
|
||||
"""
|
||||
Perform the research phase of blog generation.
|
||||
|
||||
Args:
|
||||
search_keywords (str): Keywords to research
|
||||
search_params (dict): Search parameters
|
||||
update_progress (function): Function to update progress
|
||||
|
||||
Returns:
|
||||
tuple: Google search results, Tavily search results, success flags, and blog titles
|
||||
"""
|
||||
update_progress(1, 5, f"Starting web research on '{search_keywords}'")
|
||||
logger.info(f"Researching and Writing Blog on keywords: {search_keywords}")
|
||||
|
||||
# Create a section header for the research phase
|
||||
st.subheader("🔍 Web Research Progress")
|
||||
|
||||
# Use a collapsible expander for research details
|
||||
with st.expander("Research Details", expanded=True):
|
||||
example_blog_titles = []
|
||||
|
||||
# Create a status element for research updates
|
||||
with st.status("Web research in progress...", expanded=True) as status:
|
||||
status.update(label=f"📊 Performing web research on: {search_keywords}")
|
||||
|
||||
# Create status container and progress tracking for Google SERP
|
||||
status_container = st.empty()
|
||||
research_progress = st.progress(0)
|
||||
|
||||
# Google Search
|
||||
status.update(label="🔍 Performing Google search...")
|
||||
google_search_result, g_titles, google_search_success = perform_google_search(
|
||||
search_keywords, search_params, status, status_container, research_progress
|
||||
)
|
||||
if g_titles:
|
||||
example_blog_titles.append(g_titles)
|
||||
status.update(label=f"✅ Google search complete - found {len(g_titles)} relevant resources")
|
||||
else:
|
||||
status.update(label="⚠️ Google search yielded limited results")
|
||||
|
||||
# Tavily Search
|
||||
status.update(label="🔍 Performing Tavily AI search...")
|
||||
tavily_search_result, tavily_search_success = perform_tavily_search(
|
||||
search_keywords, search_params, status
|
||||
)
|
||||
|
||||
if tavily_search_success:
|
||||
status.update(label="✅ Tavily AI search complete", state="complete")
|
||||
elif google_search_success:
|
||||
status.update(label="⚠️ Tavily search had issues, but Google search was successful")
|
||||
else:
|
||||
status.update(label="❌ Both search methods encountered issues", state="error")
|
||||
|
||||
# Clear the progress indicators
|
||||
status_container.empty()
|
||||
research_progress.empty()
|
||||
|
||||
return google_search_result, tavily_search_result, google_search_success, tavily_search_success, example_blog_titles
|
||||
|
||||
|
||||
def generate_content_phase(search_keywords, google_search_result, tavily_search_result,
|
||||
google_search_success, tavily_search_success, blog_params, update_progress):
|
||||
"""
|
||||
Generate blog content from research results.
|
||||
|
||||
Args:
|
||||
search_keywords (str): Keywords to research
|
||||
google_search_result: Results from Google search
|
||||
tavily_search_result: Results from Tavily search
|
||||
google_search_success (bool): Whether Google search was successful
|
||||
tavily_search_success (bool): Whether Tavily search was successful
|
||||
blog_params (dict): Blog parameters
|
||||
update_progress (function): Function to update progress
|
||||
|
||||
Returns:
|
||||
str: Generated blog content or None if generation failed
|
||||
"""
|
||||
# Import content generation function here to avoid circular import
|
||||
from .ai_blog_generator_utils import generate_blog_content
|
||||
|
||||
update_progress(2, 5, "Generating blog content from research")
|
||||
|
||||
# Create a section header for the content generation phase
|
||||
st.subheader("✍️ Content Generation Progress")
|
||||
|
||||
# Use a collapsible expander for content generation details
|
||||
with st.expander("Content Generation Details", expanded=True):
|
||||
# Create a status element for content generation updates
|
||||
with st.status("Content generation in progress...", expanded=True) as status:
|
||||
if google_search_success:
|
||||
source = "Google search results"
|
||||
else:
|
||||
source = "Tavily AI research"
|
||||
|
||||
status.update(label=f"📝 Creating {blog_params.get('blog_tone')} {blog_params.get('blog_type')} content for {blog_params.get('blog_demographic')} audience...")
|
||||
|
||||
blog_markdown_str = generate_blog_content(
|
||||
search_keywords, google_search_result, tavily_search_result,
|
||||
google_search_success, tavily_search_success, blog_params, status
|
||||
)
|
||||
|
||||
if blog_markdown_str:
|
||||
status.update(label=f"✅ Successfully generated ~{len(blog_markdown_str.split())} words of content using {source}", state="complete")
|
||||
else:
|
||||
status.update(label="❌ Content generation failed", state="error")
|
||||
|
||||
return blog_markdown_str
|
||||
|
||||
|
||||
def generate_metadata_and_image(blog_markdown_str, search_keywords, blog_tags, update_progress):
|
||||
"""
|
||||
Generate metadata and featured image for the blog.
|
||||
|
||||
Args:
|
||||
blog_markdown_str (str): Blog content
|
||||
search_keywords (str): Keywords used for research
|
||||
blog_tags (list): Blog tags
|
||||
update_progress (function): Function to update progress
|
||||
|
||||
Returns:
|
||||
tuple: Blog metadata and image filepath
|
||||
"""
|
||||
# Import metadata and image generation functions here to avoid circular import
|
||||
from .ai_blog_generator_utils import generate_blog_metadata, generate_blog_image
|
||||
|
||||
update_progress(3, 5, "Generating SEO metadata and enhancements")
|
||||
|
||||
# Create a section header for the enhancement phase
|
||||
st.subheader("🔍 SEO & Enhancement Progress")
|
||||
|
||||
# Use a collapsible expander for enhancement details
|
||||
with st.expander("Enhancement Details", expanded=True):
|
||||
blog_title = None
|
||||
blog_meta_desc = None
|
||||
blog_categories = None
|
||||
blog_hashtags = None
|
||||
blog_slug = None
|
||||
generated_image_filepath = None
|
||||
saved_blog_to_file = None
|
||||
|
||||
# Create a status element for enhancement updates
|
||||
with st.status("Enhancing content...", expanded=True) as status:
|
||||
# Generate metadata
|
||||
status.update(label="🏷️ Generating SEO metadata (title, description, tags)...")
|
||||
blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = generate_blog_metadata(
|
||||
blog_markdown_str, search_keywords, status
|
||||
)
|
||||
|
||||
# Check if there are updated values in session state
|
||||
if 'blog_title' in st.session_state:
|
||||
blog_title = st.session_state.blog_title
|
||||
status.update(label=f"✅ Using refined title: \"{blog_title}\"")
|
||||
|
||||
if 'blog_meta_desc' in st.session_state:
|
||||
blog_meta_desc = st.session_state.blog_meta_desc
|
||||
status.update(label=f"✅ Using refined meta description")
|
||||
|
||||
if blog_title and blog_meta_desc:
|
||||
status.update(label=f"✅ Generated metadata: \"{blog_title}\"")
|
||||
|
||||
# Generate featured image
|
||||
status.update(label="🖼️ Creating featured image...")
|
||||
generated_image_filepath = generate_blog_image(
|
||||
blog_title, blog_meta_desc, blog_markdown_str, status, blog_tags
|
||||
)
|
||||
|
||||
# Save blog content to file
|
||||
status.update(label="💾 Saving blog content...")
|
||||
saved_blog_to_file = save_blog_content(
|
||||
blog_markdown_str, blog_title, blog_meta_desc, blog_tags,
|
||||
blog_categories, generated_image_filepath, status, blog_hashtags, blog_slug
|
||||
)
|
||||
|
||||
status.update(label="✅ Content enhancement complete", state="complete")
|
||||
else:
|
||||
status.update(label="⚠️ Metadata generation had issues, using simplified format", state="warning")
|
||||
|
||||
# Add buttons for metadata refinement
|
||||
create_metadata_refinement_ui()
|
||||
|
||||
# Add rich snippet section
|
||||
create_structured_data_ui()
|
||||
|
||||
metadata = {
|
||||
"blog_title": blog_title,
|
||||
"blog_meta_desc": blog_meta_desc,
|
||||
"blog_tags": blog_tags,
|
||||
"blog_categories": blog_categories,
|
||||
"blog_hashtags": blog_hashtags,
|
||||
"blog_slug": blog_slug
|
||||
}
|
||||
|
||||
return metadata, generated_image_filepath, saved_blog_to_file
|
||||
|
||||
|
||||
def create_metadata_refinement_ui():
|
||||
"""Create UI elements for refining blog metadata (title and meta description)."""
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
if st.button("🔄 Refine Blog Title", key="refine_title_main", use_container_width=True):
|
||||
st.session_state.show_title_dialog = True
|
||||
st.rerun()
|
||||
with col2:
|
||||
if st.button("🔄 Refine Meta Description", key="refine_meta_main", use_container_width=True):
|
||||
st.session_state.show_meta_dialog = True
|
||||
st.rerun()
|
||||
|
||||
|
||||
def create_structured_data_ui():
|
||||
"""Create UI elements for generating structured data."""
|
||||
st.markdown("---")
|
||||
structured_data_col1, structured_data_col2 = st.columns([3, 1])
|
||||
|
||||
with structured_data_col1:
|
||||
# Educational popover explaining why rich snippets are important
|
||||
with st.expander("ℹ️ Why Rich Snippets Are Important for SEO"):
|
||||
st.markdown("""
|
||||
### Rich Snippets: Boosting Your SEO and Click-Through Rates
|
||||
|
||||
**What are Rich Snippets?**
|
||||
|
||||
Rich snippets are enhanced search results that display additional information directly in search engine results pages (SERPs). They're created using structured data markup (JSON-LD) that helps search engines understand your content better.
|
||||
|
||||
**Why are they important?**
|
||||
|
||||
1. **Increased Visibility**: Rich snippets stand out in search results with stars, images, and additional information
|
||||
|
||||
2. **Higher Click-Through Rates (CTR)**: Studies show rich snippets can increase CTR by 30-150%
|
||||
|
||||
3. **Improved SEO**: They help search engines understand your content better, potentially improving rankings
|
||||
|
||||
4. **Enhanced User Experience**: Users can see key information before clicking, leading to more qualified traffic
|
||||
|
||||
5. **Mobile-Friendly**: Rich snippets are especially effective on mobile searches
|
||||
|
||||
**Common types of rich snippets include:**
|
||||
- Articles/Blogs (with author, date, image)
|
||||
- Products (with ratings, price, availability)
|
||||
- Recipes (with cooking time, ratings, calories)
|
||||
- Events (with date, location, ticket info)
|
||||
- Local Business (with address, hours, ratings)
|
||||
|
||||
Adding structured data to your content is a powerful SEO technique that requires minimal effort but provides significant benefits.
|
||||
""")
|
||||
|
||||
with structured_data_col2:
|
||||
# Button to generate rich snippet
|
||||
if st.button("📊 Generate Rich Snippet", key="snippet_main", use_container_width=True):
|
||||
st.session_state.show_snippet_dialog = True
|
||||
st.rerun()
|
||||
|
||||
|
||||
def display_featured_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags, generated_image_filepath):
|
||||
"""
|
||||
Display the featured image with regeneration options.
|
||||
|
||||
Args:
|
||||
blog_title (str): Blog title
|
||||
blog_meta_desc (str): Blog meta description
|
||||
blog_markdown_str (str): Blog content
|
||||
blog_tags (list): Blog tags
|
||||
generated_image_filepath (str): Path to the generated image
|
||||
|
||||
Returns:
|
||||
str: Updated image filepath if regenerated, otherwise original filepath
|
||||
"""
|
||||
# Import image regeneration function here to avoid circular import
|
||||
from .ai_blog_generator_utils import regenerate_blog_image
|
||||
|
||||
st.subheader("🖼️ Featured Image")
|
||||
image_container = st.container()
|
||||
|
||||
# Display featured image
|
||||
with image_container:
|
||||
if generated_image_filepath:
|
||||
st.image(generated_image_filepath, caption=blog_title or "Featured Image", use_column_width=True)
|
||||
|
||||
# Add regenerate button
|
||||
if st.button("🔄 Regenerate Image", key="regenerate_image"):
|
||||
new_image_path = regenerate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags)
|
||||
if new_image_path:
|
||||
return new_image_path
|
||||
else:
|
||||
st.info("No featured image was generated. Click below to generate one.")
|
||||
if st.button("🖼️ Generate Image", key="generate_image"):
|
||||
new_image_path = regenerate_blog_image(blog_title, blog_meta_desc, blog_markdown_str, blog_tags)
|
||||
if new_image_path:
|
||||
return new_image_path
|
||||
|
||||
return generated_image_filepath
|
||||
|
||||
|
||||
def display_blog_content_and_audio(blog_markdown_str, saved_blog_to_file):
|
||||
"""
|
||||
Display the blog content and audio generation option.
|
||||
|
||||
Args:
|
||||
blog_markdown_str (str): Blog content
|
||||
saved_blog_to_file (str): Path to the saved blog file
|
||||
"""
|
||||
# Display blog content
|
||||
st.markdown("## Content")
|
||||
st.markdown(blog_markdown_str)
|
||||
|
||||
# Show file save information if available
|
||||
if saved_blog_to_file:
|
||||
st.success(f"✅ Blog saved to: {saved_blog_to_file}")
|
||||
|
||||
# Add the audio generation button
|
||||
st.markdown("---")
|
||||
audio_col1, audio_col2 = st.columns([1, 3])
|
||||
with audio_col1:
|
||||
generate_audio_button = st.button("🔊 Generate Audio Version", use_container_width=True)
|
||||
|
||||
with audio_col2:
|
||||
if generate_audio_button:
|
||||
generate_audio_version(blog_markdown_str)
|
||||
|
||||
|
||||
def display_final_metadata_table(metadata, update_progress):
|
||||
"""
|
||||
Display the final metadata table and options.
|
||||
|
||||
Args:
|
||||
metadata (dict): Blog metadata
|
||||
update_progress (function): Function to update progress
|
||||
"""
|
||||
update_progress(4, 5, "Preparing final blog presentation")
|
||||
|
||||
st.markdown("---")
|
||||
# Display metadata in a collapsible expander to save space
|
||||
with st.expander("🏷️ Metadata", expanded=True):
|
||||
st.table({
|
||||
"Metadata": ["Blog Title", "Meta Description", "Tags", "Categories", "Hashtags", "Slug"],
|
||||
"Value": [
|
||||
metadata["blog_title"],
|
||||
metadata["blog_meta_desc"],
|
||||
metadata["blog_tags"],
|
||||
metadata["blog_categories"],
|
||||
metadata["blog_hashtags"],
|
||||
metadata["blog_slug"]
|
||||
]
|
||||
})
|
||||
|
||||
# Add buttons in columns for refining metadata
|
||||
create_metadata_refinement_ui()
|
||||
|
||||
# Add a row for structured data with a "Generate Rich Snippet" button
|
||||
st.markdown("---")
|
||||
st.markdown("### Get Structured Data")
|
||||
|
||||
# Add structured data UI
|
||||
create_structured_data_ui()
|
||||
|
||||
# Create snippet generation dialog if button is clicked
|
||||
if st.session_state.get("show_snippet_dialog", False):
|
||||
display_structured_data_dialog(metadata["blog_title"], metadata["blog_tags"])
|
||||
|
||||
|
||||
def display_structured_data_dialog(blog_title, blog_tags):
|
||||
"""
|
||||
Display the structured data generation dialog.
|
||||
|
||||
Args:
|
||||
blog_title (str): Blog title
|
||||
blog_tags (list): Blog tags
|
||||
"""
|
||||
with st.expander("Structured Data Generation Tool", expanded=True):
|
||||
st.subheader("Generate Structured Data (Rich Snippets)")
|
||||
|
||||
# Close button at the top
|
||||
if st.button("Close", key="close_structured_data"):
|
||||
st.session_state.show_snippet_dialog = False
|
||||
st.rerun()
|
||||
|
||||
# Simplified blog URL input
|
||||
blog_url = st.text_input(
|
||||
"Blog URL:",
|
||||
placeholder="https://yourblog.com/your-article",
|
||||
help="Enter the URL where this blog will be published"
|
||||
)
|
||||
|
||||
# Auto-fill content type to "Article" since we're working with a blog
|
||||
content_type = "Article"
|
||||
st.info(f"Content Type: {content_type} (Auto-selected for blog content)")
|
||||
|
||||
# Form for additional article details
|
||||
with st.form(key="structured_data_form"):
|
||||
st.markdown("#### Article Details")
|
||||
|
||||
# Pre-fill with blog title and other metadata
|
||||
article_title = st.text_input("Headline:", value=blog_title if blog_title else "")
|
||||
article_author = st.text_input("Author:", value="")
|
||||
article_date = st.date_input("Date Published:", value=datetime.now())
|
||||
article_keywords = st.text_input("Keywords:", value=blog_tags if blog_tags else "")
|
||||
|
||||
submit_structured_data = st.form_submit_button("Generate JSON-LD")
|
||||
|
||||
if submit_structured_data:
|
||||
if not blog_url:
|
||||
st.error("Please enter a blog URL to generate structured data.")
|
||||
else:
|
||||
# Create details dictionary
|
||||
details = {
|
||||
"Headline": article_title,
|
||||
"Author": article_author,
|
||||
"Date Published": article_date,
|
||||
"Keywords": article_keywords
|
||||
}
|
||||
|
||||
# Call the imported ai_structured_data function or recreate its functionality
|
||||
with st.spinner("Generating structured data..."):
|
||||
# Import and use the function from the module directly
|
||||
from ...ai_seo_tools.seo_structured_data import generate_json_data
|
||||
|
||||
# Generate the structured data
|
||||
structured_data = generate_json_data(content_type, details, blog_url)
|
||||
|
||||
if structured_data:
|
||||
st.success("✅ Structured data generated successfully!")
|
||||
st.markdown("### Generated JSON-LD Code")
|
||||
st.code(structured_data, language="json")
|
||||
|
||||
# Download button
|
||||
st.download_button(
|
||||
label="📥 Download JSON-LD",
|
||||
data=structured_data,
|
||||
file_name=f"{content_type}_structured_data.json",
|
||||
mime="application/json",
|
||||
)
|
||||
|
||||
# Implementation instructions
|
||||
with st.expander("How to Implement This Code"):
|
||||
st.markdown("""
|
||||
### Adding this JSON-LD to your website:
|
||||
|
||||
1. **Copy the generated JSON-LD code** above
|
||||
|
||||
2. **Add it to the `<head>` section of your HTML** like this:
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
[PASTE YOUR JSON-LD CODE HERE]
|
||||
</script>
|
||||
```
|
||||
|
||||
3. **Verify the implementation** using Google's Rich Results Test tool:
|
||||
[https://search.google.com/test/rich-results](https://search.google.com/test/rich-results)
|
||||
|
||||
4. **Monitor your search appearance** in Google Search Console
|
||||
""")
|
||||
else:
|
||||
st.error("Failed to generate structured data. Please check your inputs and try again.")
|
||||
|
||||
|
||||
def display_title_refinement_dialog(blog_title, blog_tags):
|
||||
"""
|
||||
Display a dialog for refining the blog title.
|
||||
|
||||
Args:
|
||||
blog_title (str): Current blog title
|
||||
blog_tags (list): Blog tags for context
|
||||
"""
|
||||
with st.expander("Blog Title Refinement Tool", expanded=True):
|
||||
st.subheader("Generate Better Blog Titles")
|
||||
|
||||
# Form for title generation
|
||||
with st.form(key="title_generation_form"):
|
||||
st.markdown("#### Title Generation Parameters")
|
||||
|
||||
# Pre-fill with blog tags if available
|
||||
keywords = st.text_input("Target Keywords:",
|
||||
value=blog_tags if blog_tags else "",
|
||||
help="Enter primary keywords to target in the title")
|
||||
|
||||
blog_type = st.selectbox(
|
||||
"Blog Type:",
|
||||
["How-to Guide", "Tutorial", "List Post", "Informational", "Case Study", "Opinion Piece", "Review"],
|
||||
index=0,
|
||||
help="Select the type of blog you're creating"
|
||||
)
|
||||
|
||||
search_intent = st.selectbox(
|
||||
"Search Intent:",
|
||||
["Informational", "Commercial", "Navigational", "Transactional"],
|
||||
index=0,
|
||||
help="Select the primary search intent your title should address"
|
||||
)
|
||||
|
||||
language = st.selectbox(
|
||||
"Language:",
|
||||
["English", "Spanish", "French", "German", "Italian"],
|
||||
index=0
|
||||
)
|
||||
|
||||
submit_title = st.form_submit_button("Generate Title Suggestions")
|
||||
|
||||
if submit_title:
|
||||
with st.spinner("Generating title suggestions..."):
|
||||
# Import and use the function from the module
|
||||
from ...ai_seo_tools.content_title_generator import generate_blog_titles
|
||||
|
||||
# Generate the titles
|
||||
title_suggestions = generate_blog_titles(
|
||||
target_keywords=keywords,
|
||||
blog_type=blog_type,
|
||||
search_intent=search_intent,
|
||||
language=language
|
||||
)
|
||||
|
||||
if title_suggestions:
|
||||
st.success("✅ Generated title suggestions!")
|
||||
|
||||
# Display each title with an option to select it
|
||||
st.markdown("### Select a Title or Modify")
|
||||
|
||||
selected_title = st.text_input(
|
||||
"Selected or Modified Title:",
|
||||
value=blog_title if blog_title else (title_suggestions[0] if title_suggestions else ""),
|
||||
help="Select one of the suggested titles or modify it to your preference"
|
||||
)
|
||||
|
||||
if st.button("Confirm Title"):
|
||||
st.session_state.blog_title = selected_title
|
||||
st.session_state.show_title_dialog = False
|
||||
st.success(f"Title updated to: {selected_title}")
|
||||
st.rerun()
|
||||
|
||||
# Display all suggestions
|
||||
for i, title in enumerate(title_suggestions):
|
||||
st.markdown(f"**Option {i+1}:** {title}")
|
||||
else:
|
||||
st.error("Failed to generate title suggestions. Please try different parameters.")
|
||||
|
||||
|
||||
def display_meta_description_dialog(blog_meta_desc, blog_tags):
|
||||
"""
|
||||
Display a dialog for refining the meta description.
|
||||
|
||||
Args:
|
||||
blog_meta_desc (str): Current meta description
|
||||
blog_tags (list): Blog tags for context
|
||||
"""
|
||||
with st.expander("Meta Description Refinement Tool", expanded=True):
|
||||
st.subheader("Generate Optimized Meta Descriptions")
|
||||
|
||||
# Form for meta description generation
|
||||
with st.form(key="meta_desc_generation_form"):
|
||||
st.markdown("#### Meta Description Parameters")
|
||||
|
||||
# Pre-fill with blog tags if available
|
||||
keywords = st.text_input("Target Keywords:",
|
||||
value=blog_tags if blog_tags else "",
|
||||
help="Enter primary keywords to target in the meta description")
|
||||
|
||||
tone = st.selectbox(
|
||||
"Tone:",
|
||||
["Informative", "Engaging", "Professional", "Conversational", "Humorous", "Urgent"],
|
||||
index=0,
|
||||
help="Select the tone for your meta description"
|
||||
)
|
||||
|
||||
search_intent = st.selectbox(
|
||||
"Search Intent:",
|
||||
["Informational", "Commercial", "Navigational", "Transactional"],
|
||||
index=0,
|
||||
help="Select the primary search intent your meta description should address"
|
||||
)
|
||||
|
||||
language = st.selectbox(
|
||||
"Language:",
|
||||
["English", "Spanish", "French", "German", "Italian"],
|
||||
index=0
|
||||
)
|
||||
|
||||
submit_meta = st.form_submit_button("Generate Meta Description Suggestions")
|
||||
|
||||
if submit_meta:
|
||||
with st.spinner("Generating meta description suggestions..."):
|
||||
# Import and use the function from the module
|
||||
from ...ai_seo_tools.meta_desc_generator import generate_blog_metadesc
|
||||
|
||||
# Generate the meta descriptions
|
||||
meta_suggestions = generate_blog_metadesc(
|
||||
target_keywords=keywords,
|
||||
tone=tone,
|
||||
search_intent=search_intent,
|
||||
language=language
|
||||
)
|
||||
|
||||
if meta_suggestions:
|
||||
st.success("✅ Generated meta description suggestions!")
|
||||
|
||||
# Display each meta description with an option to select it
|
||||
st.markdown("### Select a Meta Description or Modify")
|
||||
|
||||
selected_meta = st.text_area(
|
||||
"Selected or Modified Meta Description:",
|
||||
value=blog_meta_desc if blog_meta_desc else (meta_suggestions[0] if meta_suggestions else ""),
|
||||
height=100,
|
||||
help="Select one of the suggested meta descriptions or modify it to your preference"
|
||||
)
|
||||
|
||||
if st.button("Confirm Meta Description"):
|
||||
st.session_state.blog_meta_desc = selected_meta
|
||||
st.session_state.show_meta_dialog = False
|
||||
st.success(f"Meta description updated!")
|
||||
st.rerun()
|
||||
|
||||
# Display all suggestions
|
||||
for i, meta in enumerate(meta_suggestions):
|
||||
st.markdown(f"**Option {i+1}:** {meta}")
|
||||
else:
|
||||
st.error("Failed to generate meta description suggestions. Please try different parameters.")
|
||||
|
||||
|
||||
def write_blog_from_keywords(search_keywords, url=None, search_params=None, blog_params=None):
|
||||
"""
|
||||
This function will take a blog Topic to first generate sections for it
|
||||
and then generate content for each section.
|
||||
|
||||
Args:
|
||||
search_keywords (str): Keywords to research and write about
|
||||
url (str, optional): Optional URL to use as a source
|
||||
search_params (dict, optional): Dictionary of search parameters including:
|
||||
- max_results: Maximum number of search results (default: 10)
|
||||
- search_depth: "basic" or "advanced" search depth (default: "basic")
|
||||
- include_domains: List of domains to prioritize in search
|
||||
- time_range: Time range for results (default: "year")
|
||||
blog_params (dict, optional): Dictionary of blog content characteristics including:
|
||||
- blog_length: Target word count (default: 2000)
|
||||
- blog_tone: Tone of the content (default: "Professional")
|
||||
- blog_demographic: Target audience (default: "Professional")
|
||||
- blog_type: Type of blog post (default: "Informational")
|
||||
- blog_language: Language for the blog (default: "English")
|
||||
- blog_output_format: Format for the blog (default: "markdown")
|
||||
"""
|
||||
# Check if we need to display any dialog boxes first
|
||||
if st.session_state.get("show_title_dialog") and "blog_title" in st.session_state:
|
||||
display_title_refinement_dialog(st.session_state.blog_title, None)
|
||||
return None
|
||||
|
||||
if st.session_state.get("show_meta_dialog") and "blog_meta_desc" in st.session_state:
|
||||
display_meta_description_dialog(st.session_state.blog_meta_desc, None)
|
||||
return None
|
||||
|
||||
if st.session_state.get("show_snippet_dialog"):
|
||||
# Get blog title and tags to pass to the dialog
|
||||
blog_title = st.session_state.get("blog_title", "")
|
||||
blog_tags = st.session_state.get("blog_tags", "")
|
||||
display_structured_data_dialog(blog_title, blog_tags)
|
||||
return None
|
||||
|
||||
# Initialize parameters with defaults
|
||||
search_params, blog_params = initialize_parameters(search_params, blog_params)
|
||||
|
||||
# Set up progress tracking
|
||||
final_content_placeholder, progress_placeholder, progress_bar, status_text, update_progress = setup_progress_tracking()
|
||||
|
||||
# STEP 1: Research phase
|
||||
google_search_result, tavily_search_result, google_search_success, tavily_search_success, example_blog_titles = perform_research_phase(
|
||||
search_keywords, search_params, update_progress
|
||||
)
|
||||
|
||||
# Check if both searches failed - if so, stop the process
|
||||
if not google_search_success and not tavily_search_success:
|
||||
update_progress(5, 5, "Research failed")
|
||||
progress_placeholder.error("⛔ Both Google SERP and Tavily AI searches failed. Unable to generate blog content.")
|
||||
st.warning("Please check your API keys in the environment settings and try again.")
|
||||
st.stop()
|
||||
return None
|
||||
|
||||
# STEP 2: Content generation phase
|
||||
blog_markdown_str = generate_content_phase(
|
||||
search_keywords, google_search_result, tavily_search_result,
|
||||
google_search_success, tavily_search_success, blog_params, update_progress
|
||||
)
|
||||
|
||||
if not blog_markdown_str:
|
||||
update_progress(5, 5, "Content generation failed")
|
||||
progress_placeholder.error("⛔ Failed to generate blog content from research data.")
|
||||
st.stop()
|
||||
return None
|
||||
|
||||
# STEP 3: Metadata & enhancement phase
|
||||
metadata, generated_image_filepath, saved_blog_to_file = generate_metadata_and_image(
|
||||
blog_markdown_str, search_keywords, None, update_progress
|
||||
)
|
||||
|
||||
# Display image with regeneration option
|
||||
updated_image_filepath = display_featured_image(
|
||||
metadata["blog_title"], metadata["blog_meta_desc"],
|
||||
blog_markdown_str, metadata["blog_tags"], generated_image_filepath
|
||||
)
|
||||
|
||||
if updated_image_filepath != generated_image_filepath:
|
||||
generated_image_filepath = updated_image_filepath
|
||||
st.rerun() # Refresh the page to show the new image
|
||||
|
||||
# Display blog content and audio option
|
||||
display_blog_content_and_audio(blog_markdown_str, saved_blog_to_file)
|
||||
|
||||
# STEP 4: Final presentation
|
||||
with final_content_placeholder.container():
|
||||
display_final_metadata_table(metadata, update_progress)
|
||||
|
||||
# If there's a button click to generate a structured data snippet, handle it
|
||||
if st.session_state.get("show_snippet_dialog", False):
|
||||
display_structured_data_dialog(metadata["blog_title"], metadata["blog_tags"])
|
||||
|
||||
# Final progress update
|
||||
update_progress(5, 5, "Blog generation complete!")
|
||||
|
||||
# Replace progress bar with success message
|
||||
progress_placeholder.success("✅ Blog generation process completed successfully!")
|
||||
|
||||
return blog_markdown_str
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,758 +0,0 @@
|
||||
"""
|
||||
Letter Templates Module
|
||||
|
||||
This module provides structured templates and guidance for generating
|
||||
different types and subtypes of letters.
|
||||
Templates are defined as a nested dictionary containing 'structure' (list of sections)
|
||||
and 'guidance' (a string) for each letter type and subtype.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
|
||||
# Define letter templates using a nested dictionary structure for better organization and lookup.
|
||||
# The structure is {letter_type: {subtype: {template_details}}}
|
||||
# 'default' subtype is used as a fallback if a specific subtype isn't found for a given type.
|
||||
TEMPLATES: Dict[str, Dict[str, Dict[str, Any]]] = {
|
||||
"personal": {
|
||||
"congratulations": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express congratulations",
|
||||
"Acknowledge the achievement",
|
||||
"Share personal thoughts/memory (optional)",
|
||||
"Look to the future/well wishes",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, sincere, and specific about the achievement. Express genuine happiness for the recipient. Keep the tone personal and friendly."
|
||||
},
|
||||
"thank_you": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express gratitude clearly",
|
||||
"Specify what you are thankful for",
|
||||
"Explain the impact or how you used it (optional)",
|
||||
"Share a personal thought or memory (optional)",
|
||||
"Offer reciprocation or look to the future",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be specific about what you're thankful for and how it affected you. Express sincere appreciation. Personalize the message."
|
||||
},
|
||||
"sympathy": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express sympathy for the loss",
|
||||
"Acknowledge the significance of the person/situation",
|
||||
"Share a positive memory or quality (optional)",
|
||||
"Offer specific support (optional)",
|
||||
"Closing with comforting words"
|
||||
],
|
||||
"guidance": "Be gentle, compassionate, and sincere. Avoid clichés. Focus on offering genuine comfort and acknowledging the recipient's feelings."
|
||||
},
|
||||
"apology": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Clearly state your apology",
|
||||
"Acknowledge the specific mistake or action",
|
||||
"Express understanding of the impact on the other person",
|
||||
"Explain (briefly, without making excuses) what happened (optional)",
|
||||
"Offer amends or suggest how to make things right",
|
||||
"Assure it won't happen again",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be sincere, take full responsibility for your actions, and focus on making things right. Avoid making excuses or blaming others."
|
||||
},
|
||||
"invitation": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Clearly state the invitation",
|
||||
"Provide full event details (What, When, Where)",
|
||||
"Explain the significance or purpose (optional)",
|
||||
"Mention who else might be there (optional)",
|
||||
"Request RSVP (date and contact method)",
|
||||
"Express anticipation",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be clear and specific about the details (what, when, where, why). Make it easy for the person to respond."
|
||||
},
|
||||
"friendship": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express appreciation for the friendship",
|
||||
"Share a recent memory or anecdote",
|
||||
"Acknowledge the value of the relationship",
|
||||
"Check in on them or share updates",
|
||||
"Look to the future (getting together, etc.)",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, personal, and specific about what you value in the friendship. Share updates and show genuine interest."
|
||||
},
|
||||
"love": {
|
||||
"structure": [
|
||||
"Greeting (Terms of endearment)",
|
||||
"Express depth of feelings",
|
||||
"Share a cherished memory or moment",
|
||||
"Describe specific qualities you love and appreciate",
|
||||
"Reaffirm commitment or future hopes",
|
||||
"Closing (Terms of endearment)"
|
||||
],
|
||||
"guidance": "Be sincere, personal, and specific about your feelings. Use sensory details and emotional language appropriate for your relationship."
|
||||
},
|
||||
"encouragement": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Acknowledge the situation or challenge they face",
|
||||
"Express belief in their abilities/strength",
|
||||
"Offer specific words of encouragement or support",
|
||||
"Remind them of past successes (optional)",
|
||||
"Offer practical help (optional)",
|
||||
"Look to the future with hope",
|
||||
"Closing with support"
|
||||
],
|
||||
"guidance": "Be positive, supportive, and specific about the person's strengths and abilities. Offer genuine encouragement and belief in them."
|
||||
},
|
||||
"farewell": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"State the purpose (saying goodbye)",
|
||||
"Express feelings about their departure (sadness, happiness for them)",
|
||||
"Share a positive memory or highlight their contribution",
|
||||
"Express good wishes for their future endeavors",
|
||||
"Look to staying in touch (optional)",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, reflective, and forward-looking. Focus on positive memories and express genuine good wishes for their next steps."
|
||||
},
|
||||
# Default personal letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Introduction",
|
||||
"Main content paragraphs",
|
||||
"Closing thoughts",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be personal, authentic, and appropriate for your relationship with the recipient. The tone is typically informal to semi-formal."
|
||||
}
|
||||
},
|
||||
"formal": {
|
||||
"application": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (if known)",
|
||||
"Subject line (Clear and concise)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State position applied for and where you saw it)",
|
||||
"Body paragraphs (Highlight relevant skills and experience)",
|
||||
"Closing paragraph (Reiterate interest, mention enclosed resume, call to action)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)",
|
||||
"Enclosures (Mention if attaching resume/portfolio)"
|
||||
],
|
||||
"guidance": "Be professional, concise, and specific about your qualifications and genuine interest in the position. Tailor it to the specific job description."
|
||||
},
|
||||
"complaint": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Clearly state it's a complaint)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the purpose: complaint about X service/product)",
|
||||
"Problem description (Provide specific details: date, time, location, product details, names if applicable)",
|
||||
"Impact statement (Explain how the problem affected you)",
|
||||
"Requested resolution (Clearly state what you want: refund, replacement, action)",
|
||||
"Closing paragraph (Reference attached documents, state expectation for response)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be clear, factual, and specific about the issue and your desired resolution. Maintain a respectful but firm tone. Include all relevant details."
|
||||
},
|
||||
"request": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Clearly state the request)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the purpose: making a request)",
|
||||
"Request details (Clearly explain what you are requesting)",
|
||||
"Justification (Explain why the request is necessary or beneficial)",
|
||||
"Provide supporting information (optional)",
|
||||
"Closing paragraph (Express gratitude for consideration, reiterate call to action)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and courteous about your request. Explain why it's important or beneficial to the recipient or organization."
|
||||
},
|
||||
"recommendation": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Letter of Recommendation for [Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your name, title, relationship to the recommendee, and for what purpose the letter is written)",
|
||||
"Body paragraphs (Describe the recommendee's qualifications, skills, and achievements with specific examples)",
|
||||
"Highlight relevant experiences and contributions",
|
||||
"Closing recommendation (Summarize endorsement, strongly recommend the person)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be specific, positive, and credible. Use concrete examples and anecdotes to support your recommendation. Tailor it to the specific role/opportunity."
|
||||
},
|
||||
"resignation": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Immediate supervisor/HR)",
|
||||
"Subject line (Letter of Resignation - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"Statement of resignation (Clearly state you are resigning)",
|
||||
"Last day of employment (Specify the date)",
|
||||
"Gratitude and reflection (Optional: Express thanks for the opportunity/experience)",
|
||||
"Transition plan/Offer of assistance (Optional: Suggest how to ensure a smooth handover)",
|
||||
"Closing paragraph (Express good wishes for the company's future)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be professional, positive (if possible), and clear about your departure and last day. Maintain a good relationship."
|
||||
},
|
||||
"inquiry": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Clearly state the nature of the inquiry)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your purpose for writing - making an inquiry)",
|
||||
"Inquiry details (Provide necessary context or background)",
|
||||
"Specific questions (List your questions clearly, perhaps numbered)",
|
||||
"Closing paragraph (Express gratitude for assistance, indicate when you need a response)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and courteous about your inquiry. Organize your questions logically for easy answering."
|
||||
},
|
||||
"authorization": {
|
||||
"structure": [
|
||||
"Sender's contact information (The grantor of authority)",
|
||||
"Date",
|
||||
"Recipient's contact information (The person/entity receiving the letter)",
|
||||
"Subject line (Letter of Authorization)",
|
||||
"Salutation (Formal)",
|
||||
"Statement of authorization (Clearly state who is authorized)",
|
||||
"Authorized person's details (Full name, ID if applicable)",
|
||||
"Scope of authority (Precisely define what they are authorized to do)",
|
||||
"Limitations (Specify any restrictions or conditions)",
|
||||
"Duration of authorization (Start and end dates, if applicable)",
|
||||
"Closing paragraph (State responsibility, express confidence)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and precise about who is authorized, what they can do, for how long, and under what conditions. This is a legal document."
|
||||
},
|
||||
"appeal": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Appeals committee/relevant authority)",
|
||||
"Subject line (Letter of Appeal - [Your Name] - [Subject of Appeal])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your name, the decision being appealed, and the date of the decision)",
|
||||
"Grounds for appeal (Clearly state the reasons why you believe the decision is incorrect)",
|
||||
"Provide supporting evidence (Reference attached documents: records, photos, etc.)",
|
||||
"Explain mitigating circumstances (Optional)",
|
||||
"Requested outcome (Clearly state what resolution you seek)",
|
||||
"Closing paragraph (Express hope for reconsideration, gratitude for time)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be respectful, factual, and persuasive. Focus on valid grounds for appeal and provide clear, supporting evidence. Maintain a formal tone."
|
||||
},
|
||||
"introduction": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Introduction - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (Introduce yourself and the purpose of the letter)",
|
||||
"Background information (Briefly describe your relevant background or expertise)",
|
||||
"Reason for reaching out (Explain why you are introducing yourself to this specific person/entity)",
|
||||
"Potential areas of collaboration or shared interest (Optional)",
|
||||
"Call to action (Suggest a meeting, call, or further communication)",
|
||||
"Closing paragraph (Express enthusiasm for potential connection)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be professional, informative, and engaging. Clearly explain who you are, your expertise, and why you're reaching out to them specifically."
|
||||
},
|
||||
# Default formal letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Sender's address",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be professional, clear, and concise. Use formal language and structure. The tone is typically formal."
|
||||
}
|
||||
},
|
||||
"business": {
|
||||
"sales": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Benefit-oriented)",
|
||||
"Salutation",
|
||||
"Attention-grabbing opening (Address a pain point or introduce a benefit)",
|
||||
"Problem statement (Briefly describe the challenge the recipient faces)",
|
||||
"Solution presentation (Introduce your product/service as the solution)",
|
||||
"Benefits and features (Explain how your solution helps, focusing on benefits)",
|
||||
"Social proof (Optional: Testimonials, case studies, data)",
|
||||
"Call to action (Clearly state what you want them to do next)",
|
||||
"Closing paragraph (Reiterate benefit, create urgency/incentive)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)",
|
||||
"Enclosures (Optional: Brochure, pricing)"
|
||||
],
|
||||
"guidance": "Be persuasive, customer-focused, and clear about the value proposition. Focus on benefits, not just features. Make the call to action obvious."
|
||||
},
|
||||
"proposal": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Clear and descriptive)",
|
||||
"Salutation",
|
||||
"Introduction (State purpose: submitting a proposal)",
|
||||
"Problem statement/Needs assessment (Demonstrate understanding of client's needs)",
|
||||
"Proposed solution (Describe your solution in detail)",
|
||||
"Implementation plan (Outline steps and timeline)",
|
||||
"Costs and investment (Clearly state pricing and payment terms)",
|
||||
"Benefits and ROI (Explain the value the client will receive)",
|
||||
"Call to action (Suggest next steps: meeting, discussion)",
|
||||
"Closing paragraph (Express enthusiasm, availability for questions)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)",
|
||||
"Enclosures (Proposal document, appendix)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and persuasive about your solution. Focus on the client's needs and the value you provide. Structure it logically."
|
||||
},
|
||||
"order": {
|
||||
"structure": [
|
||||
"Letterhead (Your company)",
|
||||
"Date",
|
||||
"Recipient's address (Supplier)",
|
||||
"Subject line (Purchase Order - [PO Number])",
|
||||
"Salutation",
|
||||
"Introduction (Reference quote/agreement, state purpose: placing an order)",
|
||||
"Order details (Item list with quantities, descriptions, unit prices, total)",
|
||||
"Delivery requirements (Shipping address, requested delivery date, shipping method)",
|
||||
"Payment terms (Reference agreed terms)",
|
||||
"Closing paragraph (Express expectation for timely delivery)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and detailed about what you're ordering, quantities, delivery requirements, and payment terms. Include a purchase order number."
|
||||
},
|
||||
"quotation": {
|
||||
"structure": [
|
||||
"Letterhead (Your company)",
|
||||
"Date",
|
||||
"Recipient's address (Customer)",
|
||||
"Subject line (Quotation for [Product/Service])",
|
||||
"Salutation",
|
||||
"Introduction (Reference inquiry, state purpose: providing a quotation)",
|
||||
"Quotation details (List items/services, descriptions, unit prices, quantities, line totals)",
|
||||
"Pricing breakdown (Mention taxes, discounts, fees separately)",
|
||||
"Terms and conditions (Payment terms, delivery terms, warranty)",
|
||||
"Validity period (State how long the quote is valid)",
|
||||
"Next steps (How they can place an order)",
|
||||
"Closing paragraph (Express hope to do business, offer further assistance)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and transparent about pricing, terms, and what's included or excluded. Make it easy for the customer to understand and accept."
|
||||
},
|
||||
"acknowledgment": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Acknowledgment of [Received Item/Request])",
|
||||
"Salutation",
|
||||
"Acknowledgment statement (Clearly state what you have received or are acknowledging)",
|
||||
"Details of what's being acknowledged (Reference number, date, brief description)",
|
||||
"Confirm understanding (Optional: Briefly restate the request/issue to show understanding)",
|
||||
"Next steps (Outline what will happen next, e.g., processing order, investigating issue)",
|
||||
"Timeline (Provide an estimated timeframe if possible)",
|
||||
"Closing paragraph (Express gratitude, offer further assistance)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be prompt, clear, and specific about what you're acknowledging. Set clear expectations for next steps and timelines."
|
||||
},
|
||||
"collection": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Invoice [Invoice Number] - Payment Due)",
|
||||
"Salutation",
|
||||
"Introduction (Reference invoice number and due date)",
|
||||
"Account status (Clearly state the outstanding amount)",
|
||||
"Payment request (Politely request payment)",
|
||||
"Payment options (Remind them how to pay)",
|
||||
"Consequences of non-payment (Optional: Briefly mention late fees or further action, depending on letter stage)",
|
||||
"Call to action (Request payment by a specific date)",
|
||||
"Closing paragraph (Express hope for prompt payment, offer to discuss)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be firm but professional. Clearly state the amount due, due date, and payment options. The tone may vary depending on how overdue the payment is."
|
||||
},
|
||||
"adjustment": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address (Customer who made a complaint)",
|
||||
"Subject line (Response to your inquiry - [Reference Number])",
|
||||
"Salutation",
|
||||
"Acknowledgment of complaint (Reference their communication and the issue)",
|
||||
"Investigation findings (Explain the outcome of your investigation)",
|
||||
"Adjustment offered (Clearly state the resolution: refund, replacement, credit, etc.)",
|
||||
"Apology (Optional: Express regret for the inconvenience)",
|
||||
"Preventive measures (Optional: Explain steps taken to prevent recurrence)",
|
||||
"Closing paragraph (Express hope for continued business, offer further assistance)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be responsive, empathetic, and solution-oriented. Clearly explain the adjustment and any preventive measures taken."
|
||||
},
|
||||
"credit": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address (Applicant)",
|
||||
"Subject line (Credit Application Status - [Applicant Name])",
|
||||
"Salutation",
|
||||
"Introduction (Reference their credit application and the purpose of the letter)",
|
||||
"Credit decision (Clearly state if credit is approved or denied)",
|
||||
"If approved: Credit terms (Credit limit, payment terms, interest rates)",
|
||||
"If denied: Reason for decision (Provide specific, compliant reasons)",
|
||||
"Requirements (If approved: any further steps or documents needed)",
|
||||
"Closing paragraph (If approved: Express welcome; If denied: Offer alternative options or appeals process)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and transparent about the credit decision, terms, limits, or reasons for denial. Ensure compliance with regulations if denying credit."
|
||||
},
|
||||
"follow_up": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Following up on [Previous Communication/Meeting])",
|
||||
"Salutation",
|
||||
"Reference to previous communication (Mention date, topic, or meeting)",
|
||||
"Purpose of follow-up (Clearly state why you are writing again)",
|
||||
"Action items/Next steps (Remind of agreed-upon actions or propose next steps)",
|
||||
"Provide additional information (Optional)",
|
||||
"Call to action (If applicable, e.g., request a response, schedule a meeting)",
|
||||
"Closing paragraph (Reiterate interest, express anticipation)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and action-oriented. Reference previous communication and clearly state the purpose of your follow-up and desired outcome."
|
||||
},
|
||||
# Default business letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be professional, clear, and concise. Focus on the business purpose of your letter. The tone is typically formal to semi-formal."
|
||||
}
|
||||
},
|
||||
"cover": {
|
||||
"standard": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information (if known)",
|
||||
"Subject line (Job Application - [Your Name] - [Job Title])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the position you are applying for, where you saw the advertisement, and a brief statement of enthusiasm)",
|
||||
"Body paragraph 1 (Highlight skills and experience directly relevant to the job description - often 1-2 key qualifications)",
|
||||
"Body paragraph 2 (Provide a specific example or anecdote demonstrating your abilities)",
|
||||
"Body paragraph 3 (Connect your passion/goals to the company's mission/values - optional but effective)",
|
||||
"Closing paragraph (Reiterate interest, mention enclosed resume, call to action)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be professional, specific about your most relevant qualifications, and clear about your interest in the position. Tailor every cover letter to the specific job and company."
|
||||
},
|
||||
"career_change": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Job Application - [Your Name] - [Job Title])",
|
||||
"Salutation",
|
||||
"Introduction (State the position and acknowledge your career transition)",
|
||||
"Body paragraph 1 (Highlight transferable skills from previous roles)",
|
||||
"Body paragraph 2 (Explain your motivation for the career change and how your skills apply)",
|
||||
"Body paragraph 3 (Demonstrate understanding of the new industry/role)",
|
||||
"Closing paragraph (Reiterate enthusiasm, mention enclosed resume, call to action)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Focus on transferable skills and explain your career transition. Connect your past experience and new skills directly to the requirements of the target role."
|
||||
},
|
||||
"entry_level": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Job Application - [Your Name] - [Job Title])",
|
||||
"Salutation",
|
||||
"Introduction (State the position and your enthusiasm for the opportunity as a recent graduate/entrant)",
|
||||
"Body paragraph 1 (Highlight relevant education, coursework, GPA if strong)",
|
||||
"Body paragraph 2 (Describe relevant internships, projects, or volunteer experience)",
|
||||
"Body paragraph 3 (Showcase soft skills: teamwork, communication, eagerness to learn)",
|
||||
"Closing paragraph (Reiterate interest, mention attached resume, express availability for interview)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Emphasize education, relevant internships/projects, and transferable skills gained through academic or extracurricular activities. Show strong potential and enthusiasm."
|
||||
},
|
||||
"executive": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Senior Executive/Board Member)",
|
||||
"Subject line (Executive Application - [Your Name] - [Position])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State position applying for, brief summary of executive profile)",
|
||||
"Body paragraph 1 (Highlight strategic leadership experience and key achievements)",
|
||||
"Body paragraph 2 (Discuss relevant industry expertise and market insights)",
|
||||
"Body paragraph 3 (Describe experience in driving growth, managing teams, achieving results)",
|
||||
"Closing paragraph (Reiterate interest, express desire to discuss contribution to the organization)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Emphasize strategic leadership experience, significant achievements with measurable results, and industry expertise. Use a confident, authoritative, and forward-looking tone."
|
||||
},
|
||||
"creative": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Application - [Your Name] - [Creative Role])",
|
||||
"Salutation",
|
||||
"Creative introduction (Engaging hook related to the role or your passion)",
|
||||
"Body paragraph 1 (Highlight relevant creative experience and skills)",
|
||||
"Body paragraph 2 (Reference specific portfolio pieces or projects that showcase your style/abilities)",
|
||||
"Body paragraph 3 (Describe your creative process or approach)",
|
||||
"Closing paragraph (Reiterate enthusiasm, mention attached resume/portfolio link, call to action)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Use a more engaging and expressive style appropriate for a creative role while maintaining professionalism. Highlight specific creative achievements and link to your portfolio."
|
||||
},
|
||||
"technical": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Application - [Your Name] - [Technical Role])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State position, source, and brief technical interest)",
|
||||
"Body paragraph 1 (Highlight specific technical skills and proficiencies relevant to the job description)",
|
||||
"Body paragraph 2 (Describe relevant technical projects or challenges you've solved)",
|
||||
"Body paragraph 3 (Discuss problem-solving abilities and experience with relevant technologies)",
|
||||
"Closing paragraph (Reiterate interest, mention attached resume, express availability for technical discussion/interview)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Focus on technical skills, relevant projects, and problem-solving abilities. Use appropriate technical terminology accurately."
|
||||
},
|
||||
"academic": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Search Committee Chair)",
|
||||
"Subject line (Application for [Position] - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the position, the department, and express your strong interest)",
|
||||
"Body paragraph 1 (Discuss your research experience, focus on key projects and contributions)",
|
||||
"Body paragraph 2 (Describe your teaching philosophy and relevant teaching experience)",
|
||||
"Body paragraph 3 (Mention publications, presentations, grants, and other scholarly contributions)",
|
||||
"Closing paragraph (Reiterate enthusiasm for joining the faculty, express availability for interview/presentation)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Focus on research experience, teaching philosophy, publications, and contributions to the field. Use a scholarly and professional tone suitable for academia."
|
||||
},
|
||||
"remote": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Remote Application - [Your Name] - [Job Title])",
|
||||
"Salutation",
|
||||
"Introduction (State the remote position, source, and enthusiasm for remote work)",
|
||||
"Body paragraph 1 (Highlight experience working remotely or independently)",
|
||||
"Body paragraph 2 (Emphasize self-management, time management, and organizational skills required for remote work)",
|
||||
"Body paragraph 3 (Describe strong written and verbal communication skills, essential for remote collaboration)",
|
||||
"Closing paragraph (Reiterate interest in the remote role, mention attached resume, express availability for video interview)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Emphasize self-motivation, excellent communication skills (especially written), time management, and any prior experience working independently or in remote teams."
|
||||
},
|
||||
"referral": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Referral Application - [Your Name] - [Job Title] - Referred by [Referrer's Name])",
|
||||
"Salutation",
|
||||
"Referral introduction (Immediately state who referred you and for what position)",
|
||||
"Body paragraph 1 (Briefly explain your connection to the referrer and how you learned about the role)",
|
||||
"Body paragraph 2 (Highlight key qualifications relevant to the job description)",
|
||||
"Body paragraph 3 (Express strong interest in the position and the company)",
|
||||
"Closing paragraph (Reiterate enthusiasm, mention attached resume, express availability for interview)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Mention the referral prominently and early. Explain your connection to the referrer and how it aligns with your interest in the role. Still, ensure you highlight your own qualifications."
|
||||
},
|
||||
# Default cover letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Contact information",
|
||||
"Date",
|
||||
"Recipient's information",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be professional, specific about your qualifications, and clear about your interest in the position. Tailor your letter to the specific job and company."
|
||||
}
|
||||
},
|
||||
# Overall default template if letter type is not recognized
|
||||
"default": {
|
||||
"structure": [
|
||||
"Introduction",
|
||||
"Body",
|
||||
"Conclusion"
|
||||
],
|
||||
"guidance": "Be clear, concise, and appropriate for your audience and purpose. This is a generic structure."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_template_by_type(letter_type: str, subtype: str = "default") -> Dict[str, Any]:
|
||||
"""
|
||||
Get a template for a specific letter type and subtype using a dictionary lookup.
|
||||
|
||||
Args:
|
||||
letter_type: Type of letter (e.g., "personal", "formal", "business", "cover").
|
||||
subtype: Subtype of letter (e.g., "congratulations", "application", "sales").
|
||||
Defaults to "default" if no subtype is specified.
|
||||
|
||||
Returns:
|
||||
Template dictionary with 'structure' (List[str]) and 'guidance' (str).
|
||||
Returns the default template if the letter type or subtype is not found,
|
||||
ensuring the return structure is always consistent.
|
||||
"""
|
||||
# Get templates for the specific letter type, or the overall default templates
|
||||
# .get() method is used for safe dictionary access with a default fallback
|
||||
type_templates = TEMPLATES.get(letter_type, TEMPLATES["default"])
|
||||
|
||||
# Get the template for the specific subtype, or the default for that letter type
|
||||
# Chain .get() calls to handle cases where subtype or the type's default is missing
|
||||
template = type_templates.get(subtype, type_templates.get("default", TEMPLATES["default"]))
|
||||
|
||||
# Ensure the returned template always has 'structure' (as a list) and 'guidance' (as a string) keys.
|
||||
# This adds robustness in case a template definition is incomplete.
|
||||
if "structure" not in template or not isinstance(template["structure"], list):
|
||||
# Fallback structure if missing or incorrect type
|
||||
template["structure"] = ["Introduction", "Body", "Conclusion"]
|
||||
# Update guidance to reflect that the structure was defaulted
|
||||
template["guidance"] = "Generic template structure applied due to missing or invalid definition."
|
||||
|
||||
if "guidance" not in template or not isinstance(template["guidance"], str):
|
||||
# Fallback guidance if missing or incorrect type
|
||||
template["guidance"] = "Generic guidance applied due to missing or invalid definition."
|
||||
|
||||
|
||||
return template
|
||||
|
||||
# Example usage (for testing purposes)
|
||||
if __name__ == '__main__':
|
||||
# Test cases to demonstrate functionality and default handling
|
||||
print("--- Testing Letter Templates Module ---")
|
||||
|
||||
# Test a known personal letter subtype
|
||||
personal_congrats = get_template_by_type("personal", "congratulations")
|
||||
print("\nPersonal Congratulations Template:")
|
||||
print(f"Structure: {personal_congrats['structure']}")
|
||||
print(f"Guidance: {personal_congrats['guidance']}")
|
||||
|
||||
# Test a known formal letter subtype
|
||||
formal_complaint = get_template_by_type("formal", "complaint")
|
||||
print("\nFormal Complaint Template:")
|
||||
print(f"Structure: {formal_complaint['structure']}")
|
||||
print(f"Guidance: {formal_complaint['guidance']}")
|
||||
|
||||
# Test a known business letter subtype
|
||||
business_sales = get_template_by_type("business", "sales")
|
||||
print("\nBusiness Sales Template:")
|
||||
print(f"Structure: {business_sales['structure']}")
|
||||
print(f"Guidance: {business_sales['guidance']}")
|
||||
|
||||
# Test a known cover letter subtype
|
||||
cover_entry_level = get_template_by_type("cover", "entry_level")
|
||||
print("\nCover Entry Level Template:")
|
||||
print(f"Structure: {cover_entry_level['structure']}")
|
||||
print(f"Guidance: {cover_entry_level['guidance']}")
|
||||
|
||||
# Test an unknown letter type (should fallback to overall default)
|
||||
unknown_type = get_template_by_type("unknown_type", "some_subtype")
|
||||
print("\nUnknown Type Template (Should be Overall Default):")
|
||||
print(f"Structure: {unknown_type['structure']}")
|
||||
print(f"Guidance: {unknown_type['guidance']}")
|
||||
|
||||
# Test a known letter type but unknown subtype (should fallback to type's default)
|
||||
personal_unknown_subtype = get_template_by_type("personal", "unknown_subtype")
|
||||
print("\nPersonal Unknown Subtype Template (Should be Personal Default):")
|
||||
print(f"Structure: {personal_unknown_subtype['structure']}")
|
||||
print(f"Guidance: {personal_unknown_subtype['guidance']}")
|
||||
|
||||
# Test with only letter type (should use type's default)
|
||||
formal_default = get_template_by_type("formal")
|
||||
print("\nFormal Default Template (No Subtype Specified):")
|
||||
print(f"Structure: {formal_default['structure']}")
|
||||
print(f"Guidance: {formal_default['guidance']}")
|
||||
@@ -1,236 +0,0 @@
|
||||
"""
|
||||
AI Letter Writer - Main Module
|
||||
|
||||
This module provides a comprehensive interface for generating various types of letters
|
||||
using AI assistance. It supports multiple letter formats, styles, and use cases.
|
||||
It uses Streamlit for the user interface.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
# Assuming these modules exist in a package structure
|
||||
from .letter_types import (
|
||||
business_letters,
|
||||
personal_letters,
|
||||
formal_letters,
|
||||
cover_letters,
|
||||
recommendation_letters,
|
||||
complaint_letters,
|
||||
thank_you_letters,
|
||||
invitation_letters
|
||||
)
|
||||
# Assuming these utility functions exist
|
||||
from .utils.letter_formatter import format_letter
|
||||
from .utils.letter_analyzer import analyze_letter_tone, check_formality
|
||||
from .utils.letter_templates import get_template_by_type
|
||||
|
||||
# Define the letter types and their properties
|
||||
LETTER_TYPES_CONFIG = [
|
||||
{
|
||||
"id": "business",
|
||||
"name": "Business Letters",
|
||||
"icon": "💼",
|
||||
"description": "Professional correspondence for business contexts.",
|
||||
"color": "#1E88E5", # Blue 600
|
||||
"module": business_letters
|
||||
},
|
||||
{
|
||||
"id": "personal",
|
||||
"name": "Personal Letters",
|
||||
"icon": "💌",
|
||||
"description": "Heartfelt messages for friends and family.",
|
||||
"color": "#43A047", # Green 600
|
||||
"module": personal_letters
|
||||
},
|
||||
{
|
||||
"id": "formal",
|
||||
"name": "Formal Letters",
|
||||
"icon": "📜",
|
||||
"description": "Official correspondence for institutions and authorities.",
|
||||
"color": "#5E35B1", # Deep Purple 600
|
||||
"module": formal_letters
|
||||
},
|
||||
{
|
||||
"id": "cover",
|
||||
"name": "Cover Letters",
|
||||
"icon": "📋",
|
||||
"description": "Job application letters to showcase your qualifications.",
|
||||
"color": "#FB8C00", # Orange 600
|
||||
"module": cover_letters
|
||||
},
|
||||
{
|
||||
"id": "recommendation",
|
||||
"name": "Recommendation Letters",
|
||||
"icon": "👍",
|
||||
"description": "Endorse colleagues, students, or employees.",
|
||||
"color": "#00ACC1", # Cyan 600
|
||||
"module": recommendation_letters
|
||||
},
|
||||
{
|
||||
"id": "complaint",
|
||||
"name": "Complaint Letters",
|
||||
"icon": "⚠️",
|
||||
"description": "Address issues with products, services, or situations.",
|
||||
"color": "#E53935", # Red 600
|
||||
"module": complaint_letters
|
||||
},
|
||||
{
|
||||
"id": "thank_you",
|
||||
"name": "Thank You Letters",
|
||||
"icon": "🙏",
|
||||
"description": "Express gratitude for various occasions.",
|
||||
"color": "#8E24AA", # Purple 600
|
||||
"module": thank_you_letters
|
||||
},
|
||||
{
|
||||
"id": "invitation",
|
||||
"name": "Invitation Letters",
|
||||
"icon": "🎉",
|
||||
"description": "Invite people to events, interviews, or gatherings.",
|
||||
"color": "#FFB300", # Amber 600
|
||||
"module": invitation_letters
|
||||
}
|
||||
]
|
||||
|
||||
# Map letter type IDs to their modules for easy access
|
||||
LETTER_MODULES_MAP = {config["id"]: config["module"] for config in LETTER_TYPES_CONFIG}
|
||||
|
||||
|
||||
def initialize_session_state() -> None:
|
||||
"""Initializes necessary Streamlit session state variables."""
|
||||
if "letter_type" not in st.session_state:
|
||||
st.session_state.letter_type = None
|
||||
if "letter_subtype" not in st.session_state:
|
||||
st.session_state.letter_subtype = None # Useful if a letter type has subtypes
|
||||
if "generated_letter" not in st.session_state:
|
||||
st.session_state.generated_letter = None
|
||||
if "letter_metadata" not in st.session_state:
|
||||
# Store information like sender, recipient, date, subject, tone, etc.
|
||||
st.session_state.letter_metadata = {}
|
||||
if "letter_input_data" not in st.session_state:
|
||||
# Store user inputs for letter generation
|
||||
st.session_state.letter_input_data = {}
|
||||
|
||||
|
||||
def display_letter_type_selection() -> None:
|
||||
"""Displays the letter type selection interface using a grid of styled containers with buttons."""
|
||||
|
||||
st.markdown("## Select Letter Type")
|
||||
|
||||
# Create a grid layout for the cards (3 columns)
|
||||
cols = st.columns(3)
|
||||
|
||||
# Display each letter type as a card with a button below it
|
||||
for i, letter_type_config in enumerate(LETTER_TYPES_CONFIG):
|
||||
with cols[i % 3]:
|
||||
# Use markdown to create a styled container for the card appearance
|
||||
st.markdown(
|
||||
f"""
|
||||
<div style="
|
||||
background-color: {letter_type_config['color']};
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px; /* Space between card content and button */
|
||||
color: white;
|
||||
min-height: 180px; /* Ensure consistent minimum height */
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between; /* Distribute space within the card */
|
||||
">
|
||||
<h3 style="margin-top: 0; color: white;">{letter_type_config['icon']} {letter_type_config['name']}</h3>
|
||||
<p style="color: white;">{letter_type_config['description']}</p>
|
||||
</div>
|
||||
""",
|
||||
unsafe_allow_html=True
|
||||
)
|
||||
|
||||
# Place the Streamlit button below the styled container
|
||||
# Make the button expand to the width of the column for better alignment with the card
|
||||
if st.button(
|
||||
f"Select {letter_type_config['name']}",
|
||||
key=f"btn_select_{letter_type_config['id']}", # Unique key for each button
|
||||
use_container_width=True
|
||||
):
|
||||
st.session_state.letter_type = letter_type_config['id']
|
||||
# Clear previous state data when selecting a new type
|
||||
st.session_state.letter_subtype = None
|
||||
st.session_state.generated_letter = None
|
||||
st.session_state.letter_metadata = {}
|
||||
st.session_state.letter_input_data = {}
|
||||
st.rerun()
|
||||
|
||||
|
||||
def display_letter_interface(letter_type_id: str) -> None:
|
||||
"""
|
||||
Displays the interface for the selected letter type by calling the
|
||||
appropriate module's write function.
|
||||
|
||||
Args:
|
||||
letter_type_id: The ID string of the selected letter type.
|
||||
"""
|
||||
module = LETTER_MODULES_MAP.get(letter_type_id)
|
||||
|
||||
if module:
|
||||
try:
|
||||
# Call the main function (e.g., write_letter or main) from the selected module
|
||||
# Assuming the module has a function that renders its UI and handles generation
|
||||
module.write_letter() # Assuming the function is named 'write_letter'
|
||||
except AttributeError:
|
||||
st.error(f"Module for '{letter_type_id}' does not have a 'write_letter' function.")
|
||||
except Exception as e:
|
||||
st.error(f"An error occurred while loading the interface for '{letter_type_id}': {e}")
|
||||
else:
|
||||
st.error(f"Letter type module '{letter_type_id}' not found in map.")
|
||||
|
||||
|
||||
def write_letter() -> None:
|
||||
"""Main function for the AI Letter Writer interface."""
|
||||
|
||||
# Page title and description
|
||||
st.title("✉️ AI Letter Writer")
|
||||
st.markdown("""
|
||||
Create professional, personalized letters for any occasion. Select a letter type below to get started.
|
||||
Our AI will help you craft the perfect letter with the right tone, structure, and content.
|
||||
""")
|
||||
|
||||
# Initialize session state on first run
|
||||
initialize_session_state()
|
||||
|
||||
# Back button logic - only show if a letter type is selected
|
||||
if st.session_state.letter_type is not None:
|
||||
if st.button("← Back to Letter Types"):
|
||||
# Reset session state to return to selection
|
||||
st.session_state.letter_type = None
|
||||
st.session_state.letter_subtype = None
|
||||
st.session_state.generated_letter = None
|
||||
st.session_state.letter_metadata = {}
|
||||
st.session_state.letter_input_data = {}
|
||||
st.rerun() # Rerun to show the selection page
|
||||
|
||||
# Main navigation logic
|
||||
if st.session_state.letter_type is None:
|
||||
# Display letter type selection if no type is selected
|
||||
display_letter_type_selection()
|
||||
else:
|
||||
# Display the interface for the selected letter type
|
||||
display_letter_interface(st.session_state.letter_type)
|
||||
|
||||
# --- Placeholder for displaying generated letter and actions ---
|
||||
# This part would typically be handled within the specific letter type modules
|
||||
# after the letter is generated. However, if a common display is needed
|
||||
# after returning from the module function, it would go here, but this
|
||||
# requires the module function to somehow signal completion or store
|
||||
# the generated letter in session state. The current structure expects
|
||||
# the module's write_letter() to handle its entire lifecycle.
|
||||
|
||||
# Example of potentially displaying a generated letter after returning
|
||||
# (This assumes the module updates st.session_state.generated_letter)
|
||||
# if st.session_state.generated_letter:
|
||||
# st.subheader("Generated Letter Preview")
|
||||
# st.text_area("Your Letter", st.session_state.generated_letter, height=400)
|
||||
# # Add options like copy, download, analyze, edit, etc.
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the main letter writing function when the script is executed
|
||||
write_letter()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,493 +0,0 @@
|
||||
"""
|
||||
Letter Analyzer Utility
|
||||
|
||||
This module provides functions for analyzing letter content, including tone,
|
||||
formality, readability, and offering basic suggestions for improvement.
|
||||
Note: The analysis methods provided here are simplified rule-based and
|
||||
keyword-based approaches. For more sophisticated analysis in a production
|
||||
environment, consider using advanced Natural Language Processing (NLP)
|
||||
libraries and models.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Dict, Any, Tuple, List
|
||||
|
||||
def analyze_letter_tone(content: str) -> Dict[str, float]:
|
||||
"""
|
||||
Analyze the tone of a letter based on the presence of specific keywords
|
||||
and phrases.
|
||||
|
||||
Args:
|
||||
content: The letter content to analyze.
|
||||
|
||||
Returns:
|
||||
Dictionary with tone scores (formal, friendly, assertive, etc.).
|
||||
Scores are based on the frequency of matching patterns and capped at 1.0.
|
||||
"""
|
||||
# This is a simplified version using keyword matching.
|
||||
# A more sophisticated approach would involve NLP libraries for sentiment and tone analysis.
|
||||
|
||||
# Initialize tone scores
|
||||
# Scores are arbitrary counts normalized in a simple way
|
||||
tone_scores = {
|
||||
"formal": 0.0,
|
||||
"friendly": 0.0,
|
||||
"assertive": 0.0,
|
||||
"respectful": 0.0,
|
||||
"urgent": 0.0,
|
||||
"apologetic": 0.0
|
||||
}
|
||||
|
||||
# Define patterns for different tones (case-insensitive)
|
||||
formal_patterns = [
|
||||
r"\bI am writing to\b",
|
||||
r"\bI would like to\b",
|
||||
r"\bplease find\b",
|
||||
r"\bregarding\b",
|
||||
r"\bpursuant to\b",
|
||||
r"\bhereby\b",
|
||||
r"\bthus\b",
|
||||
r"\btherefore\b",
|
||||
r"\bfurthermore\b",
|
||||
r"\bconsequently\b",
|
||||
r"\bnevertheless\b",
|
||||
r"\bmoreover\b",
|
||||
r"\benclosed\b", # Added common formal word
|
||||
r"\bherewith\b" # Added common formal word
|
||||
]
|
||||
|
||||
friendly_patterns = [
|
||||
r"\bhope you're well\b",
|
||||
r"\bhope this finds you well\b",
|
||||
r"\bgreat to hear\b",
|
||||
r"\blooking forward\b",
|
||||
r"\bthanks\b",
|
||||
r"\bappreciate\b",
|
||||
r"!", # Exclamation points often indicate friendly or excited tone
|
||||
r"\bexcited\b",
|
||||
r"\bgreat\b", # Common friendly adjective
|
||||
r"\bnice\b" # Common friendly adjective
|
||||
]
|
||||
|
||||
assertive_patterns = [
|
||||
r"\brequire\b",
|
||||
r"\bmust\b",
|
||||
r"\bneed\b",
|
||||
r"\bexpect\b",
|
||||
r"\bdemand\b",
|
||||
r"\binsist\b",
|
||||
r"\bimmediately\b",
|
||||
r"\baction\b", # Often used in assertive contexts
|
||||
r"\bresolution\b" # Can imply assertion
|
||||
]
|
||||
|
||||
respectful_patterns = [
|
||||
r"\brespectfully\b",
|
||||
r"\bhonored\b",
|
||||
r"\bplease\b",
|
||||
r"\bkindly\b",
|
||||
r"\bgrateful\b",
|
||||
r"\bthank you\b",
|
||||
r"\bappreciate\b",
|
||||
r"\bhumbly\b", # Added respectful word
|
||||
r"\bapologies\b" # Can show respect for impact
|
||||
]
|
||||
|
||||
urgent_patterns = [
|
||||
r"\burgent\b",
|
||||
r"\bas soon as possible\b",
|
||||
r"\bASAP\b",
|
||||
r"\bimmediately\b",
|
||||
r"\bpressing\b",
|
||||
r"\bcritical\b",
|
||||
r"\bdeadline\b",
|
||||
r"\bexpedite\b", # Added urgent word
|
||||
r"\bpromptly\b" # Added urgent word
|
||||
]
|
||||
|
||||
apologetic_patterns = [
|
||||
r"\bapologize\b",
|
||||
r"\bsorry\b",
|
||||
r"\bregret\b",
|
||||
r"\bmistake\b",
|
||||
r"\berror\b",
|
||||
r"\binconvenience\b",
|
||||
r"\bfault\b", # Added apologetic word
|
||||
r"\boversight\b" # Added apologetic word
|
||||
]
|
||||
|
||||
# Count pattern matches and update scores (arbitrary weighting)
|
||||
# A simple count multiplied by a factor acts as a basic indicator
|
||||
for pattern in formal_patterns:
|
||||
tone_scores["formal"] += len(re.findall(pattern, content, re.IGNORECASE)) * 0.2
|
||||
|
||||
for pattern in friendly_patterns:
|
||||
tone_scores["friendly"] += len(re.findall(pattern, content, re.IGNORECASE)) * 0.2
|
||||
|
||||
for pattern in assertive_patterns:
|
||||
tone_scores["assertive"] += len(re.findall(pattern, content, re.IGNORECASE)) * 0.2
|
||||
|
||||
for pattern in respectful_patterns:
|
||||
tone_scores["respectful"] += len(re.findall(pattern, content, re.IGNORECASE)) * 0.2
|
||||
|
||||
for pattern in urgent_patterns:
|
||||
tone_scores["urgent"] += len(re.findall(pattern, content, re.IGNORECASE)) * 0.2
|
||||
|
||||
for pattern in apologetic_patterns:
|
||||
tone_scores["apologetic"] += len(re.findall(pattern, content, re.IGNORECASE)) * 0.2
|
||||
|
||||
# Cap scores at 1.0 (arbitrary capping)
|
||||
# A more meaningful score might be relative frequency or use a proper model
|
||||
for tone in tone_scores:
|
||||
tone_scores[tone] = min(tone_scores[tone], 1.0)
|
||||
|
||||
return tone_scores
|
||||
|
||||
def check_formality(content: str) -> float:
|
||||
"""
|
||||
Check the formality level of a letter based on the presence of formal
|
||||
vs. informal indicators and contractions.
|
||||
|
||||
Args:
|
||||
content: The letter content to analyze.
|
||||
|
||||
Returns:
|
||||
Formality score between 0.0 (very informal) and 1.0 (very formal).
|
||||
Calculated as formal_count / (formal_count + informal_count).
|
||||
"""
|
||||
# This is a simplified version based on keyword counting.
|
||||
# More accurate formality analysis would require advanced NLP techniques.
|
||||
|
||||
# Define formal and informal indicators (case-insensitive)
|
||||
formal_indicators = [
|
||||
r"\bDear\b",
|
||||
r"\bSincerely\b",
|
||||
r"\bRegards\b",
|
||||
r"\bRespectfully\b",
|
||||
r"\bI am writing to\b",
|
||||
r"\bI would like to\b",
|
||||
r"\bplease find\b",
|
||||
r"\bregarding\b",
|
||||
r"\bpursuant to\b",
|
||||
r"\bhereby\b",
|
||||
r"\bthus\b",
|
||||
r"\btherefore\b",
|
||||
r"\bfurthermore\b",
|
||||
r"\bconsequently\b",
|
||||
r"\bnevertheless\b",
|
||||
r"\bmoreover\b",
|
||||
r"\benclosed\b",
|
||||
r"\bherewith\b",
|
||||
r"\bsincerely yours\b", # Added
|
||||
r"\bto whom it may concern\b" # Added
|
||||
]
|
||||
|
||||
informal_indicators = [
|
||||
r"\bHey\b",
|
||||
r"\bHi\b",
|
||||
r"\bWhat's up\b",
|
||||
r"\bCheers\b",
|
||||
r"\bThanks\b", # 'Thank you' is formal, 'Thanks' is informal
|
||||
r"\bTake care\b",
|
||||
r"\bSee you\b",
|
||||
r"\bLater\b",
|
||||
r"\bBye\b",
|
||||
r"\bLove\b", # As a closing
|
||||
r"\bXO\b",
|
||||
r"!+", # Multiple exclamation points
|
||||
r"\bawesome\b",
|
||||
r"\bcool\b",
|
||||
r"\bgreat\b",
|
||||
r"\bnice\b",
|
||||
r"\bbtw\b", # By the way
|
||||
r"\bimo\b", # In my opinion
|
||||
r"\blol\b" # Laugh out loud
|
||||
]
|
||||
|
||||
# Define common contractions (case-insensitive)
|
||||
contractions = [
|
||||
r"\bdon't\b", r"\bcan't\b", r"\bwon't\b", r"\bshouldn't\b",
|
||||
r"\bcouldn't\b", r"\bwouldn't\b", r"\bhasn't\b", r"\bhaven't\b",
|
||||
r"\bisn't\b", r"\baren't\b", r"\bwasn't\b", r"\bweren't\b",
|
||||
r"\bi'm\b", r"\byou're\b", r"\bhe's\b", r"\bshe's\b", r"\bit's\b",
|
||||
r"\bwe're\b", r"\bthey're\b", r"\bi've\b", r"\byou've\b",
|
||||
r"\bwe've\b", r"\bthey've\b", r"\bi'd\b", r"\byou'd\b",
|
||||
r"\bhe'd\b", r"\bshe'd\b", r"\bit'd\b", r"\bwe'd\b", r"\bthey'd\b",
|
||||
r"\bi'll\b", r"\byou'll\b", r"\bhe'll\b", r"\bshe'll\b", r"\bit'll\b",
|
||||
r"\bwe'll\b", r"\bthey'll\b"
|
||||
]
|
||||
|
||||
formal_count = 0
|
||||
for pattern in formal_indicators:
|
||||
formal_count += len(re.findall(pattern, content, re.IGNORECASE))
|
||||
|
||||
informal_count = 0
|
||||
for pattern in informal_indicators:
|
||||
informal_count += len(re.findall(pattern, content, re.IGNORECASE))
|
||||
|
||||
# Count contractions as informal indicators
|
||||
for pattern in contractions:
|
||||
informal_count += len(re.findall(pattern, content, re.IGNORECASE))
|
||||
|
||||
# Calculate formality score
|
||||
total_indicators = formal_count + informal_count
|
||||
if total_indicators == 0:
|
||||
# If no indicators found, return a neutral score
|
||||
return 0.5
|
||||
|
||||
# Score is the proportion of formal indicators
|
||||
formality_score = formal_count / total_indicators
|
||||
return formality_score
|
||||
|
||||
def count_syllables_simple(word: str) -> int:
|
||||
"""
|
||||
Counts syllables in a word using a simplified heuristic.
|
||||
This method is not linguistically perfect but provides a basic estimate
|
||||
for readability formulas.
|
||||
|
||||
Args:
|
||||
word: The word string.
|
||||
|
||||
Returns:
|
||||
Estimated syllable count.
|
||||
"""
|
||||
word = word.lower()
|
||||
if len(word) <= 3:
|
||||
# Assume short words have one syllable
|
||||
return 1
|
||||
|
||||
# Remove common silent endings like 'e', 'es', 'ed'
|
||||
if word.endswith(('es', 'ed')):
|
||||
word = word[:-2]
|
||||
elif word.endswith('e'):
|
||||
word = word[:-1]
|
||||
|
||||
# Count vowel groups (consecutive vowels count as one syllable)
|
||||
vowels = 'aeiouy'
|
||||
count = 0
|
||||
prev_is_vowel = False
|
||||
|
||||
for char in word:
|
||||
is_vowel = char in vowels
|
||||
if is_vowel and not prev_is_vowel:
|
||||
count += 1
|
||||
prev_is_vowel = is_vowel
|
||||
|
||||
# Ensure at least one syllable is counted
|
||||
return max(1, count)
|
||||
|
||||
|
||||
def get_readability_metrics(content: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate readability metrics for a letter using simplified methods
|
||||
like Flesch Reading Ease.
|
||||
|
||||
Args:
|
||||
content: The letter content to analyze.
|
||||
|
||||
Returns:
|
||||
Dictionary with readability metrics: word_count, sentence_count,
|
||||
avg_words_per_sentence, flesch_reading_ease, reading_level.
|
||||
"""
|
||||
# Split content into words and sentences using simple regex
|
||||
words = re.findall(r'\b\w+\b', content)
|
||||
# Split by common sentence terminators, handling potential multiple marks
|
||||
sentences = re.split(r'[.!?]+\s*', content)
|
||||
# Filter out empty strings resulting from the split (e.g., trailing punctuation)
|
||||
sentences = [s for s in sentences if s.strip()]
|
||||
|
||||
word_count = len(words)
|
||||
sentence_count = len(sentences)
|
||||
syllable_count = sum(count_syllables_simple(word) for word in words)
|
||||
|
||||
if word_count == 0 or sentence_count == 0:
|
||||
return {
|
||||
"word_count": word_count,
|
||||
"sentence_count": sentence_count,
|
||||
"avg_words_per_sentence": 0.0,
|
||||
"flesch_reading_ease": 0.0,
|
||||
"reading_level": "N/A"
|
||||
}
|
||||
|
||||
# Calculate average words per sentence
|
||||
avg_words_per_sentence = word_count / sentence_count
|
||||
|
||||
# Calculate Flesch Reading Ease Score
|
||||
# Formula: 206.835 - (1.015 * AvgWordsPerSentence) - (84.6 * AvgSyllablesPerWord)
|
||||
# AvgSyllablesPerWord = syllable_count / word_count
|
||||
avg_syllables_per_word = syllable_count / word_count if word_count > 0 else 0
|
||||
|
||||
flesch = 206.835 - (1.015 * avg_words_per_sentence) - (84.6 * avg_syllables_per_word)
|
||||
# Clamp score between 0 and 100
|
||||
flesch = max(0.0, min(100.0, flesch))
|
||||
|
||||
# Determine reading level based on Flesch score ranges
|
||||
if flesch >= 90:
|
||||
reading_level = "Very Easy (5th grade)"
|
||||
elif flesch >= 80:
|
||||
reading_level = "Easy (6th grade)"
|
||||
elif flesch >= 70:
|
||||
reading_level = "Fairly Easy (7th grade)"
|
||||
elif flesch >= 60:
|
||||
reading_level = "Standard (8th-9th grade)"
|
||||
elif flesch >= 50:
|
||||
reading_level = "Fairly Difficult (10th-12th grade)"
|
||||
elif flesch >= 30:
|
||||
reading_level = "Difficult (College)"
|
||||
else:
|
||||
reading_level = "Very Difficult (Graduate)"
|
||||
|
||||
return {
|
||||
"word_count": word_count,
|
||||
"sentence_count": sentence_count,
|
||||
"avg_words_per_sentence": round(avg_words_per_sentence, 2), # Rounded for display
|
||||
"flesch_reading_ease": round(flesch, 2), # Rounded for display
|
||||
"reading_level": reading_level
|
||||
}
|
||||
|
||||
def suggest_improvements(content: str, letter_type: str) -> List[str]:
|
||||
"""
|
||||
Suggest improvements for a letter based on its content, basic analysis,
|
||||
and target letter type.
|
||||
|
||||
Args:
|
||||
content: The letter content to analyze.
|
||||
letter_type: The type of letter (e.g., "business", "cover", "personal").
|
||||
|
||||
Returns:
|
||||
List of improvement suggestions strings.
|
||||
"""
|
||||
suggestions = []
|
||||
|
||||
words = re.findall(r'\b\w+\b', content)
|
||||
word_count = len(words)
|
||||
|
||||
# Basic length check based on letter type
|
||||
if letter_type in ["business", "formal"]:
|
||||
if word_count < 100 and word_count > 10: # Avoid suggesting for very short placeholders
|
||||
suggestions.append("Consider adding more details to make your letter more comprehensive.")
|
||||
elif word_count > 600: # Increased max length slightly
|
||||
suggestions.append("Your letter is quite long. Consider condensing it for better readability and focus.")
|
||||
elif letter_type == "cover":
|
||||
if word_count < 150 and word_count > 10: # Avoid suggesting for very short placeholders
|
||||
suggestions.append("Your cover letter may be too brief. Consider highlighting more of your relevant qualifications.")
|
||||
elif word_count > 500: # Increased max length slightly
|
||||
suggestions.append("Your cover letter is quite long. Consider focusing on your most relevant qualifications and experiences.")
|
||||
elif letter_type == "recommendation":
|
||||
if word_count < 150 and word_count > 10:
|
||||
suggestions.append("Consider adding more specific examples or anecdotes to strengthen the recommendation.")
|
||||
elif word_count > 600:
|
||||
suggestions.append("Your recommendation letter is quite long. Ensure it remains focused and impactful.")
|
||||
|
||||
|
||||
# Check for overuse of "I" (simple count-based heuristic)
|
||||
# Count "I" as a standalone word
|
||||
i_count = len(re.findall(r"\bI\b", content))
|
||||
# Avoid suggestion for very short content or content with few sentences
|
||||
sentence_count = len(re.split(r'[.!?]+\s*', content.strip()))
|
||||
if sentence_count > 2 and word_count > 50 and i_count > sentence_count * 1.5: # Suggest if 'I' count is significantly higher than sentence count
|
||||
suggestions.append("Your letter contains many uses of 'I'. Consider rephrasing some sentences to focus more on the recipient or the subject matter.")
|
||||
|
||||
|
||||
# Check for expression of gratitude (using common phrases)
|
||||
gratitude_patterns = [r"\bthank you\b", r"\bgrateful\b", r"\bappreciate\b"]
|
||||
has_gratitude = any(re.search(pattern, content, re.IGNORECASE) for pattern in gratitude_patterns)
|
||||
# Suggest adding gratitude, but avoid for letter types where it might be less common (e.g., some complaint letters)
|
||||
if not has_gratitude and letter_type not in ["complaint", "urgent"]:
|
||||
suggestions.append("Consider expressing gratitude or appreciation somewhere in your letter.")
|
||||
|
||||
# Check for clear call to action (using common phrases)
|
||||
# Phrases indicating desired action or next step
|
||||
action_phrases = [
|
||||
"look forward to", "please", "would appreciate", "request",
|
||||
"hope to", "call me", "email me", "contact me", "schedule",
|
||||
"arrange", "require action", "next steps"
|
||||
]
|
||||
has_call_to_action = any(phrase in content.lower() for phrase in action_phrases)
|
||||
# Suggest adding a call to action for relevant letter types
|
||||
if not has_call_to_action and letter_type in ["business", "cover", "complaint", "invitation"]:
|
||||
suggestions.append("Consider adding a clear call to action or outlining the desired next steps.")
|
||||
|
||||
# Check for proper closing (using common phrases)
|
||||
closing_patterns = [
|
||||
r"\bSincerely\b", r"\bRegards\b", r"\bThank you\b", r"\bBest regards\b",
|
||||
r"\bYours sincerely\b", r"\bYours faithfully\b", r"\bRespectfully\b",
|
||||
r"\bBest wishes\b", r"\bKind regards\b"
|
||||
]
|
||||
# Check if any standard closing phrase is present, typically near the end
|
||||
# A more robust check might look specifically at the last paragraph/lines
|
||||
has_proper_closing = any(re.search(pattern, content[-200:], re.IGNORECASE) for pattern in closing_patterns) # Check last 200 chars
|
||||
|
||||
if not has_proper_closing and word_count > 20: # Avoid suggesting for very short snippets
|
||||
suggestions.append("Consider adding a proper closing phrase (e.g., Sincerely, Regards) followed by your name.")
|
||||
|
||||
return suggestions
|
||||
|
||||
# Example usage (for testing purposes, not part of the module's core functionality)
|
||||
if __name__ == '__main__':
|
||||
sample_formal_letter = """
|
||||
Dear Mr. Smith,
|
||||
|
||||
I am writing to follow up regarding the project proposal submitted on October 26, 2023.
|
||||
We believe the proposed solution aligns well with your stated requirements.
|
||||
Please find the revised budget document attached for your review.
|
||||
We look forward to your feedback at your earliest convenience.
|
||||
|
||||
Sincerely,
|
||||
Jane Doe
|
||||
"""
|
||||
|
||||
sample_informal_letter = """
|
||||
Hey John,
|
||||
|
||||
Hope you're doing well! Just wanted to quickly touch base about the party next week.
|
||||
Excited to catch up with everyone! Let me know if you need any help setting up.
|
||||
Thanks!
|
||||
|
||||
Best,
|
||||
Alex
|
||||
"""
|
||||
|
||||
sample_complaint_letter = """
|
||||
To Whom It May Concern,
|
||||
|
||||
I am writing to complain about the faulty product I received on November 1, 2023 (Order #12345).
|
||||
The device stopped working after only two days of use. I require a full refund or replacement immediately.
|
||||
I expect a prompt response regarding this issue.
|
||||
|
||||
Sincerely,
|
||||
Concerned Customer
|
||||
"""
|
||||
|
||||
print("--- Analyzing Formal Letter ---")
|
||||
tone = analyze_letter_tone(sample_formal_letter)
|
||||
formality = check_formality(sample_formal_letter)
|
||||
readability = get_readability_metrics(sample_formal_letter)
|
||||
suggestions = suggest_improvements(sample_formal_letter, "business")
|
||||
|
||||
print(f"Tone: {tone}")
|
||||
print(f"Formality: {formality:.2f}")
|
||||
print(f"Readability: {readability}")
|
||||
print(f"Suggestions: {suggestions}")
|
||||
|
||||
print("\n--- Analyzing Informal Letter ---")
|
||||
tone = analyze_letter_tone(sample_informal_letter)
|
||||
formality = check_formality(sample_informal_letter)
|
||||
readability = get_readability_metrics(sample_informal_letter)
|
||||
suggestions = suggest_improvements(sample_informal_letter, "personal")
|
||||
|
||||
print(f"Tone: {tone}")
|
||||
print(f"Formality: {formality:.2f}")
|
||||
print(f"Readability: {readability}")
|
||||
print(f"Suggestions: {suggestions}")
|
||||
|
||||
print("\n--- Analyzing Complaint Letter ---")
|
||||
tone = analyze_letter_tone(sample_complaint_letter)
|
||||
formality = check_formality(sample_complaint_letter)
|
||||
readability = get_readability_metrics(sample_complaint_letter)
|
||||
suggestions = suggest_improvements(sample_complaint_letter, "complaint")
|
||||
|
||||
print(f"Tone: {tone}")
|
||||
print(f"Formality: {formality:.2f}")
|
||||
print(f"Readability: {readability}")
|
||||
print(f"Suggestions: {suggestions}")
|
||||
@@ -1,545 +0,0 @@
|
||||
"""
|
||||
Letter Formatter Module
|
||||
|
||||
This module provides utilities for formatting letters and generating HTML
|
||||
previews in different styles (Personal, Formal, Business, Cover).
|
||||
The formatting functions here are primarily focused on generating HTML
|
||||
for preview purposes, applying standard layout conventions for each letter type
|
||||
using inline CSS styles.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Dict, Any
|
||||
|
||||
def format_letter(content: str, metadata: Dict[str, Any], letter_type: str = "personal") -> str:
|
||||
"""
|
||||
Format a letter with basic structure (paragraphs).
|
||||
|
||||
Args:
|
||||
content: The raw letter content (string).
|
||||
metadata: Dictionary containing metadata (currently not used for formatting in this placeholder).
|
||||
letter_type: Type of letter (personal, formal, business, cover).
|
||||
|
||||
Returns:
|
||||
Formatted letter content (currently just returns the input content).
|
||||
This is a placeholder and would be expanded to apply specific
|
||||
formatting rules (e.g., indentation, spacing) based on letter type
|
||||
and metadata in a full implementation before generating HTML.
|
||||
For this module, we primarily rely on the HTML generation functions
|
||||
to handle the visual formatting.
|
||||
"""
|
||||
# This is a basic placeholder. In a real implementation, this function
|
||||
# might process the raw text content to add indentation, adjust line breaks,
|
||||
# or handle specific markdown-like syntax before it's passed to the
|
||||
# HTML generation functions.
|
||||
# For now, we assume the input `content` uses double newlines for paragraphs.
|
||||
return content
|
||||
|
||||
def get_letter_preview_html(content: str, metadata: Dict[str, Any], letter_type: str = "personal") -> str:
|
||||
"""
|
||||
Generate HTML for letter preview based on letter type and metadata.
|
||||
This function acts as a dispatcher to the specific HTML generation functions.
|
||||
|
||||
Args:
|
||||
content: The letter content string.
|
||||
metadata: Dictionary containing metadata like sender/recipient info, date, etc.
|
||||
letter_type: Type of letter ("personal", "formal", "business", "cover").
|
||||
Defaults to "personal".
|
||||
|
||||
Returns:
|
||||
HTML string for letter preview, styled appropriately for the type.
|
||||
Includes basic styling for a printable letter appearance.
|
||||
"""
|
||||
# Dispatch to the appropriate HTML generation function based on letter type
|
||||
# Pass the content and metadata to the specific functions
|
||||
if letter_type == "personal":
|
||||
return get_personal_letter_html(content, metadata)
|
||||
elif letter_type == "formal":
|
||||
return get_formal_letter_html(content, metadata)
|
||||
elif letter_type == "business":
|
||||
return get_business_letter_html(content, metadata)
|
||||
elif letter_type == "cover":
|
||||
return get_cover_letter_html(content, metadata)
|
||||
else:
|
||||
# Fallback for unrecognized types, displaying raw content in a styled box
|
||||
return f"""
|
||||
<div style="max-width: 800px; margin: 20px auto; padding: 20px; border: 1px solid #ccc; font-family: sans-serif; line-height: 1.6; background-color: #fff8f8; color: #333; border-radius: 8px;">
|
||||
<h3 style="color: #e53935; margin-top: 0;">Preview Unavailable for Unknown Letter Type</h3>
|
||||
<p>The letter type '{letter_type}' is not recognized. Displaying raw content:</p>
|
||||
<pre style="white-space: pre-wrap; word-wrap: break-word; background-color: #f8f8f8; padding: 15px; border: 1px solid #ddd; border-radius: 4px; overflow-x: auto;">{content}</pre>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def get_personal_letter_html(content: str, metadata: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generate HTML for personal letter preview with basic styling.
|
||||
Uses a more informal layout and font style.
|
||||
|
||||
Args:
|
||||
content: The letter content string.
|
||||
metadata: Dictionary containing personal letter metadata (sender_name, date).
|
||||
|
||||
Returns:
|
||||
HTML string for personal letter preview.
|
||||
"""
|
||||
# Extract metadata with default empty strings for robustness
|
||||
sender_name = metadata.get("sender_name", "")
|
||||
# recipient_name = metadata.get("recipient_name", "") # Less common in personal body, but could be used in greeting
|
||||
date = metadata.get("date", "")
|
||||
|
||||
# Split content into paragraphs based on double newlines
|
||||
# Use list comprehension to strip whitespace and filter out empty strings
|
||||
paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()]
|
||||
|
||||
# Format paragraphs as HTML <p> tags with bottom margin
|
||||
formatted_paragraphs = "".join(f"<p style='margin-bottom: 1em;'>{paragraph}</p>" for paragraph in paragraphs)
|
||||
|
||||
# Basic HTML structure with inline styles for a personal letter feel
|
||||
# Styles aim for a warm, readable appearance
|
||||
html = f"""
|
||||
<div style="max-width: 700px; margin: 20px auto; padding: 30px; border: 1px solid #e0e0e0; border-radius: 8px; background-color: #ffffff; font-family: 'Georgia', serif; line-height: 1.7; color: #333; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div style="text-align: right; margin-bottom: 30px; font-size: 0.9em; color: #555;">
|
||||
{date if date else "[Date]"}
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 30px;">
|
||||
{formatted_paragraphs if formatted_paragraphs else "<p style='color: #888;'>Letter content goes here...</p>"}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 40px;">
|
||||
<p style="margin-bottom: 0.5em;">Sincerely,</p>
|
||||
<p style="font-weight: bold; margin-top: 0;">{sender_name if sender_name else "[Sender Name]"}</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return html
|
||||
|
||||
def get_formal_letter_html(content: str, metadata: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generate HTML for formal letter preview with standard formal structure and styling.
|
||||
Uses a more professional layout and font style (Arial/sans-serif).
|
||||
|
||||
Args:
|
||||
content: The letter content string.
|
||||
metadata: Dictionary containing formal letter metadata.
|
||||
|
||||
Returns:
|
||||
HTML string for formal letter preview.
|
||||
"""
|
||||
# Extract metadata with default empty strings
|
||||
sender_name = metadata.get("sender_name", "")
|
||||
sender_title = metadata.get("sender_title", "")
|
||||
sender_organization = metadata.get("sender_organization", "")
|
||||
# Replace newlines in address for HTML display
|
||||
sender_address = metadata.get("sender_address", "").replace("\n", "<br>")
|
||||
sender_phone = metadata.get("sender_phone", "")
|
||||
sender_email = metadata.get("sender_email", "")
|
||||
|
||||
recipient_name = metadata.get("recipient_name", "")
|
||||
recipient_title = metadata.get("recipient_title", "")
|
||||
recipient_organization = metadata.get("recipient_organization", "")
|
||||
# Replace newlines in address for HTML display
|
||||
recipient_address = metadata.get("recipient_address", "").replace("\n", "<br>")
|
||||
|
||||
date = metadata.get("date", "")
|
||||
subject = metadata.get("subject", "") # Added subject line
|
||||
salutation = metadata.get("salutation", "Dear Sir/Madam,") # Added salutation
|
||||
complimentary_close = metadata.get("complimentary_close", "Sincerely,") # Added close
|
||||
|
||||
# Determine alignment based on letter format (simplified)
|
||||
# Full Block: All aligned left
|
||||
# Modified Block: Sender address block, date, closing, and signature are right-aligned
|
||||
letter_format = metadata.get("letter_format", "Full Block")
|
||||
sender_address_align = "left"
|
||||
date_align = "left"
|
||||
closing_align = "left"
|
||||
|
||||
if letter_format == "Modified Block":
|
||||
sender_address_align = "right"
|
||||
date_align = "right"
|
||||
closing_align = "right"
|
||||
|
||||
# Split content into paragraphs based on double newlines
|
||||
paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()]
|
||||
|
||||
# Format paragraphs as HTML <p> tags with bottom margin
|
||||
formatted_paragraphs = "".join(f"<p style='margin-bottom: 1em;'>{paragraph}</p>" for paragraph in paragraphs)
|
||||
|
||||
# Basic HTML structure with inline styles for a formal letter
|
||||
html = f"""
|
||||
<div style="max-width: 800px; margin: 20px auto; padding: 30px; border: 1px solid #d0d0d0; border-radius: 8px; background-color: #ffffff; font-family: 'Arial', sans-serif; line-height: 1.6; color: #333; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
|
||||
<div style="text-align: {sender_address_align}; margin-bottom: 20px; font-size: 0.9em;">
|
||||
<p style="margin: 0;">{sender_name if sender_name else "[Sender Name]"}{', ' + sender_title if sender_title else ''}</p>
|
||||
<p style="margin: 0;">{sender_organization if sender_organization else "[Sender Organization]"}</p>
|
||||
<p style="margin: 0;">{sender_address if sender_address else "[Sender Address]"}</p>
|
||||
<p style="margin: 0;">{sender_phone}</p>
|
||||
<p style="margin: 0;">{sender_email}</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: {date_align}; margin-bottom: 20px;">
|
||||
<p style="margin: 0;">{date if date else "[Date]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px; font-size: 0.9em;">
|
||||
<p style="margin: 0;">{recipient_name if recipient_name else "[Recipient Name]"}{', ' + recipient_title if recipient_title else ''}</p>
|
||||
<p style="margin: 0;">{recipient_organization if recipient_organization else "[Recipient Organization]"}</p>
|
||||
<p style="margin: 0;">{recipient_address if recipient_address else "[Recipient Address]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0; font-weight: bold;">Subject: {subject if subject else "[Subject Line]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0;">{salutation}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
{formatted_paragraphs if formatted_paragraphs else "<p style='color: #888;'>Letter content goes here...</p>"}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 40px; text-align: {closing_align};">
|
||||
<p style="margin-bottom: 0.5em;">{complimentary_close}</p>
|
||||
<p style="font-weight: bold; margin: 0;">{sender_name}</p>
|
||||
<p style="margin: 0; font-size: 0.9em;">{sender_title}</p>
|
||||
<p style="margin: 0; font-size: 0.9em;">{sender_organization}</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return html
|
||||
|
||||
def get_business_letter_html(content: str, metadata: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generate HTML for business letter preview with standard business structure and styling.
|
||||
Includes optional letterhead.
|
||||
|
||||
Args:
|
||||
content: The letter content string.
|
||||
metadata: Dictionary containing business letter metadata.
|
||||
|
||||
Returns:
|
||||
HTML string for business letter preview.
|
||||
"""
|
||||
# Extract metadata with default empty strings
|
||||
sender_company = metadata.get("sender_company", "")
|
||||
sender_name = metadata.get("sender_name", "")
|
||||
sender_title = metadata.get("sender_title", "")
|
||||
sender_address = metadata.get("sender_address", "").replace("\n", "<br>")
|
||||
sender_phone = metadata.get("sender_phone", "")
|
||||
sender_email = metadata.get("sender_email", "")
|
||||
sender_website = metadata.get("sender_website", "")
|
||||
|
||||
recipient_company = metadata.get("recipient_company", "")
|
||||
recipient_name = metadata.get("recipient_name", "")
|
||||
recipient_title = metadata.get("recipient_title", "")
|
||||
recipient_address = metadata.get("recipient_address", "").replace("\n", "<br>")
|
||||
|
||||
date = metadata.get("date", "")
|
||||
subject = metadata.get("subject", "") # Added subject line
|
||||
salutation = metadata.get("salutation", "Dear Sir/Madam,") # Added salutation
|
||||
complimentary_close = metadata.get("complimentary_close", "Sincerely,") # Added close
|
||||
|
||||
# Determine alignment based on letter format (simplified)
|
||||
letter_format = metadata.get("letter_format", "Full Block")
|
||||
sender_info_align = "left"
|
||||
date_align = "left"
|
||||
closing_align = "left"
|
||||
|
||||
if letter_format == "Modified Block":
|
||||
sender_info_align = "right"
|
||||
date_align = "right"
|
||||
closing_align = "right"
|
||||
|
||||
# Include letterhead logic
|
||||
include_letterhead = metadata.get("include_letterhead", True)
|
||||
|
||||
# Split content into paragraphs based on double newlines
|
||||
paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()]
|
||||
|
||||
# Format paragraphs as HTML <p> tags with bottom margin
|
||||
formatted_paragraphs = "".join(f"<p style='margin-bottom: 1em;'>{paragraph}</p>" for paragraph in paragraphs)
|
||||
|
||||
# Create letterhead HTML if included and company name is provided
|
||||
letterhead_html = ""
|
||||
if include_letterhead and sender_company:
|
||||
letterhead_html = f"""
|
||||
<div style="padding-bottom: 15px; margin-bottom: 20px; border-bottom: 1px solid #eee;">
|
||||
<h2 style="margin: 0; color: #333; font-size: 1.5em;">{sender_company}</h2>
|
||||
<p style="margin: 5px 0 0 0; font-size: 0.9em; color: #555;">
|
||||
{sender_address.replace('<br>', ', ') if sender_address else ''}
|
||||
{' | ' + sender_phone if sender_phone else ''}
|
||||
{' | ' + sender_email if sender_email else ''}
|
||||
{' | ' + sender_website if sender_website else ''}
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Basic HTML structure with inline styles for a business letter
|
||||
html = f"""
|
||||
<div style="max-width: 800px; margin: 20px auto; padding: 30px; border: 1px solid #d0d0d0; border-radius: 8px; background-color: #ffffff; font-family: 'Arial', sans-serif; line-height: 1.6; color: #333; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
{letterhead_html}
|
||||
|
||||
<div style="text-align: {date_align}; margin-bottom: 20px;">
|
||||
<p style="margin: 0;">{date if date else "[Date]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px; font-size: 0.9em;">
|
||||
<p style="margin: 0;">{recipient_name if recipient_name else "[Recipient Name]"}{', ' + recipient_title if recipient_title else ''}</p>
|
||||
<p style="margin: 0;">{recipient_company if recipient_company else "[Recipient Company]"}</p>
|
||||
<p style="margin: 0;">{recipient_address if recipient_address else "[Recipient Address]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0; font-weight: bold;">Subject: {subject if subject else "[Subject Line]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0;">{salutation}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
{formatted_paragraphs if formatted_paragraphs else "<p style='color: #888;'>Letter content goes here...</p>"}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 40px; text-align: {closing_align};">
|
||||
<p style="margin-bottom: 0.5em;">{complimentary_close}</p>
|
||||
<p style="font-weight: bold; margin: 0;">{sender_name if sender_name else "[Sender Name]"}</p>
|
||||
<p style="margin: 0; font-size: 0.9em;">{sender_title}</p>
|
||||
<p style="margin: 0; font-size: 0.9em;">{sender_company}</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return html
|
||||
|
||||
def get_cover_letter_html(content: str, metadata: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generate HTML for cover letter preview with standard cover letter structure and styling.
|
||||
Includes sender contact block and optional online links.
|
||||
|
||||
Args:
|
||||
content: The letter content string.
|
||||
metadata: Dictionary containing cover letter metadata.
|
||||
|
||||
Returns:
|
||||
HTML string for cover letter preview.
|
||||
"""
|
||||
# Extract metadata with default empty strings
|
||||
sender_name = metadata.get("sender_name", "")
|
||||
sender_email = metadata.get("sender_email", "")
|
||||
sender_phone = metadata.get("sender_phone", "")
|
||||
sender_location = metadata.get("sender_location", "")
|
||||
sender_linkedin = metadata.get("sender_linkedin", "")
|
||||
sender_portfolio = metadata.get("sender_portfolio", "")
|
||||
|
||||
recipient_name = metadata.get("recipient_name", "")
|
||||
recipient_title = metadata.get("recipient_title", "") # Added recipient title
|
||||
recipient_company = metadata.get("recipient_company", "")
|
||||
recipient_department = metadata.get("recipient_department", "") # Added department
|
||||
recipient_address = metadata.get("recipient_address", "").replace("\n", "<br>") # Added recipient address
|
||||
|
||||
date = metadata.get("date", "")
|
||||
job_title = metadata.get("job_title", "") # Added job title for subject
|
||||
salutation = metadata.get("salutation", "Dear Hiring Manager,") # Added salutation
|
||||
complimentary_close = metadata.get("complimentary_close", "Sincerely,") # Added close
|
||||
|
||||
|
||||
# Split content into paragraphs based on double newlines
|
||||
paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()]
|
||||
|
||||
# Format paragraphs as HTML <p> tags with bottom margin
|
||||
formatted_paragraphs = "".join(f"<p style='margin-bottom: 1em;'>{paragraph}</p>" for paragraph in paragraphs)
|
||||
|
||||
# Construct sender contact line, only including fields that have values
|
||||
sender_contact_parts = [sender_location, sender_phone, sender_email]
|
||||
sender_contact_line = " | ".join(filter(None, sender_contact_parts))
|
||||
|
||||
# Construct sender online links line, only including fields that have values
|
||||
sender_online_parts = []
|
||||
if sender_linkedin:
|
||||
# Add basic styling for links
|
||||
sender_online_parts.append(f'<a href="{sender_linkedin}" style="color: #0077b5; text-decoration: none;">LinkedIn</a>')
|
||||
if sender_portfolio:
|
||||
# Add basic styling for links
|
||||
sender_online_parts.append(f'<a href="{sender_portfolio}" style="color: #0077b5; text-decoration: none;">Portfolio</a>')
|
||||
|
||||
sender_online_line = " | ".join(filter(None, sender_online_parts))
|
||||
|
||||
|
||||
# Basic HTML structure with inline styles for a cover letter
|
||||
# Styles aim for a clean, professional look
|
||||
html = f"""
|
||||
<div style="max-width: 800px; margin: 20px auto; padding: 30px; border: 1px solid #d0d0d0; border-radius: 8px; background-color: #ffffff; font-family: 'Arial', sans-serif; line-height: 1.6; color: #333; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
|
||||
<div style="text-align: left; margin-bottom: 30px; padding-bottom: 15px; border-bottom: 1px solid #eee;">
|
||||
<h2 style="margin: 0; color: #333; font-size: 1.5em;">{sender_name if sender_name else "[Your Name]"}</h2>
|
||||
{'<p style="margin: 5px 0 0 0; font-size: 0.9em; color: #555;">' + sender_contact_line + '</p>' if sender_contact_line else ''}
|
||||
{'<p style="margin: 2px 0 0 0; font-size: 0.9em;">' + sender_online_line + '</p>' if sender_online_line else ''}
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0;">{date if date else "[Date]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px; font-size: 0.9em;">
|
||||
<p style="margin: 0;">{recipient_name if recipient_name else "[Recipient Name]"}{', ' + recipient_title if recipient_title else ''}</p>
|
||||
<p style="margin: 0;">{recipient_department}</p>
|
||||
<p style="margin: 0;">{recipient_company if recipient_company else "[Recipient Company]"}</p>
|
||||
<p style="margin: 0;">{recipient_address if recipient_address else "[Recipient Address]"}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0; font-weight: bold;">Subject: Application for {job_title if job_title else '[Job Title]'} Position</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin: 0;">{salutation}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
{formatted_paragraphs if formatted_paragraphs else "<p style='color: #888;'>Letter content goes here...</p>"}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 40px;">
|
||||
<p style="margin-bottom: 0.5em;">{complimentary_close}</p>
|
||||
<p style="font-weight: bold; margin: 0;">{sender_name}</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
return html
|
||||
|
||||
# Example usage (for testing purposes)
|
||||
if __name__ == '__main__':
|
||||
sample_personal_content = """
|
||||
Hi Sarah,
|
||||
|
||||
Hope you're doing well!
|
||||
|
||||
Just wanted to send a quick note to say how much I enjoyed catching up last week. It was great hearing about your trip to Italy.
|
||||
|
||||
Let's try to do it again soon!
|
||||
|
||||
Best,
|
||||
Emily
|
||||
"""
|
||||
sample_personal_metadata = {
|
||||
"sender_name": "Emily Davis",
|
||||
"recipient_name": "Sarah Johnson",
|
||||
"date": "November 5, 2023"
|
||||
}
|
||||
|
||||
sample_formal_content = """
|
||||
I am writing to formally request a copy of my academic transcript.
|
||||
|
||||
I require this document for a graduate school application. The deadline for submission is December 15, 2023.
|
||||
|
||||
Please let me know if there are any fees associated with this request or if any further information is needed from my end.
|
||||
|
||||
Thank you for your time and assistance.
|
||||
"""
|
||||
sample_formal_metadata_full_block = {
|
||||
"sender_name": "John Smith",
|
||||
"sender_title": "Student",
|
||||
"sender_organization": "University of Example",
|
||||
"sender_address": "123 University Ave\nAnytown, CA 91234",
|
||||
"sender_phone": "(555) 123-4567",
|
||||
"sender_email": "john.smith@example.com",
|
||||
"recipient_name": "Registrar's Office",
|
||||
"recipient_organization": "University of Example",
|
||||
"recipient_address": "456 Admin Building\nAnytown, CA 91234",
|
||||
"date": "November 5, 2023",
|
||||
"subject": "Request for Academic Transcript",
|
||||
"salutation": "To the Registrar's Office,",
|
||||
"complimentary_close": "Sincerely,",
|
||||
"letter_format": "Full Block"
|
||||
}
|
||||
|
||||
sample_formal_metadata_modified_block = sample_formal_metadata_full_block.copy()
|
||||
sample_formal_metadata_modified_block["letter_format"] = "Modified Block"
|
||||
|
||||
|
||||
sample_business_content = """
|
||||
This letter confirms the details of Purchase Order #PO-7890.
|
||||
|
||||
We are ordering 50 units of Model X widgets at the agreed-upon price of $100 per unit, totaling $5,000.
|
||||
|
||||
Please ensure delivery to our warehouse by November 20, 2023. Payment will be made within 30 days of receipt of invoice.
|
||||
|
||||
Thank you for your prompt processing of this order.
|
||||
"""
|
||||
sample_business_metadata_full_block = {
|
||||
"sender_company": "Acme Corp",
|
||||
"sender_name": "Alice Brown",
|
||||
"sender_title": "Procurement Manager",
|
||||
"sender_address": "789 Business Rd\nMetropolis, NY 10001",
|
||||
"sender_phone": "(555) 987-6543",
|
||||
"sender_email": "alice.brown@acmecorp.com",
|
||||
"sender_website": "www.acmecorp.com",
|
||||
"recipient_company": "Supplier Co.",
|
||||
"recipient_name": "Sales Department",
|
||||
"recipient_title": "",
|
||||
"recipient_address": "101 Vendor Lane\nIndustriatown, TX 75001",
|
||||
"date": "November 5, 2023",
|
||||
"subject": "Purchase Order Confirmation - PO-7890",
|
||||
"salutation": "To the Sales Department,",
|
||||
"complimentary_close": "Sincerely,",
|
||||
"letter_format": "Full Block",
|
||||
"include_letterhead": True
|
||||
}
|
||||
|
||||
sample_business_metadata_modified_block = sample_business_metadata_full_block.copy()
|
||||
sample_business_metadata_modified_block["letter_format"] = "Modified Block"
|
||||
sample_business_metadata_no_letterhead = sample_business_metadata_full_block.copy()
|
||||
sample_business_metadata_no_letterhead["include_letterhead"] = False
|
||||
|
||||
|
||||
sample_cover_letter_content = """
|
||||
I am writing to express my enthusiastic interest in the Marketing Specialist position advertised on LinkedIn.
|
||||
|
||||
With three years of experience in digital marketing and a proven track record in content creation and social media management, I am confident in my ability to contribute to your team. My skills in [Specific Skill 1] and [Specific Skill 2] align perfectly with the requirements outlined in the job description.
|
||||
|
||||
In my previous role at [Previous Company], I successfully managed social media campaigns that resulted in a 25% increase in engagement. I am particularly drawn to [Company Name]'s innovative approach to [Industry Trend] and believe my creative problem-solving skills would be a valuable asset.
|
||||
|
||||
Thank you for considering my application. I have attached my resume for your review and welcome the opportunity to discuss how my background and skills can benefit [Company Name].
|
||||
"""
|
||||
sample_cover_letter_metadata = {
|
||||
"sender_name": "Jane Doe",
|
||||
"sender_email": "jane.doe@email.com",
|
||||
"sender_phone": "(123) 456-7890",
|
||||
"sender_location": "San Francisco, CA",
|
||||
"sender_linkedin": "https://linkedin.com/in/janedoe",
|
||||
"sender_portfolio": "https://janedoeportfolio.com",
|
||||
"recipient_name": "Hiring Manager",
|
||||
"recipient_title": "", # Example with no recipient title
|
||||
"recipient_company": "Innovative Solutions Inc.",
|
||||
"recipient_department": "Marketing Department",
|
||||
"recipient_address": "456 Tech Way\nSilicon Valley, CA 95001",
|
||||
"date": "November 5, 2023",
|
||||
"job_title": "Marketing Specialist",
|
||||
"salutation": "Dear Hiring Manager,",
|
||||
"complimentary_close": "Sincerely,"
|
||||
}
|
||||
|
||||
print("--- Personal Letter HTML Preview ---")
|
||||
print(get_letter_preview_html(sample_personal_content, sample_personal_metadata, letter_type="personal"))
|
||||
|
||||
print("\n--- Formal Letter HTML Preview (Full Block) ---")
|
||||
print(get_letter_preview_html(sample_formal_content, sample_formal_metadata_full_block, letter_type="formal"))
|
||||
|
||||
print("\n--- Formal Letter HTML Preview (Modified Block) ---")
|
||||
print(get_letter_preview_html(sample_formal_content, sample_formal_metadata_modified_block, letter_type="formal"))
|
||||
|
||||
print("\n--- Business Letter HTML Preview (Full Block, with Letterhead) ---")
|
||||
print(get_letter_preview_html(sample_business_content, sample_business_metadata_full_block, letter_type="business"))
|
||||
|
||||
print("\n--- Business Letter HTML Preview (Modified Block, with Letterhead) ---")
|
||||
print(get_letter_preview_html(sample_business_content, sample_business_metadata_modified_block, letter_type="business"))
|
||||
|
||||
print("\n--- Business Letter HTML Preview (Full Block, no Letterhead) ---")
|
||||
print(get_letter_preview_html(sample_business_content, sample_business_metadata_no_letterhead, letter_type="business"))
|
||||
|
||||
print("\n--- Cover Letter HTML Preview ---")
|
||||
print(get_letter_preview_html(sample_cover_letter_content, sample_cover_letter_metadata, letter_type="cover"))
|
||||
|
||||
print("\n--- Unknown Type HTML Preview ---")
|
||||
print(get_letter_preview_html("Some random content.", {}, letter_type="unknown"))
|
||||
@@ -1,988 +0,0 @@
|
||||
"""
|
||||
Letter Templates Module
|
||||
|
||||
This module provides structured templates and guidance for generating
|
||||
different types and subtypes of letters.
|
||||
Templates are defined as dictionaries containing a 'structure' (list of sections)
|
||||
and 'guidance' (a string).
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
|
||||
# Define letter templates using a nested dictionary structure for easier management
|
||||
TEMPLATES: Dict[str, Dict[str, Dict[str, Any]]] = {
|
||||
"personal": {
|
||||
"congratulations": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express congratulations",
|
||||
"Acknowledge the achievement",
|
||||
"Share personal thoughts/memory (optional)",
|
||||
"Look to the future/well wishes",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, sincere, and specific about the achievement. Express genuine happiness for the recipient. Keep the tone personal and friendly."
|
||||
},
|
||||
"thank_you": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express gratitude clearly",
|
||||
"Specify what you are thankful for",
|
||||
"Explain the impact or how you used it (optional)",
|
||||
"Share a personal thought or memory (optional)",
|
||||
"Offer reciprocation or look to the future",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be specific about what you're thankful for and how it affected you. Express sincere appreciation. Personalize the message."
|
||||
},
|
||||
"sympathy": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express sympathy for the loss",
|
||||
"Acknowledge the significance of the person/situation",
|
||||
"Share a positive memory or quality (optional)",
|
||||
"Offer specific support (optional)",
|
||||
"Closing with comforting words"
|
||||
],
|
||||
"guidance": "Be gentle, compassionate, and sincere. Avoid clichés. Focus on offering genuine comfort and acknowledging the recipient's feelings."
|
||||
},
|
||||
"apology": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Clearly state your apology",
|
||||
"Acknowledge the specific mistake or action",
|
||||
"Express understanding of the impact on the other person",
|
||||
"Explain (briefly, without making excuses) what happened (optional)",
|
||||
"Offer amends or suggest how to make things right",
|
||||
"Assure it won't happen again",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be sincere, take full responsibility for your actions, and focus on making things right. Avoid making excuses or blaming others."
|
||||
},
|
||||
"invitation": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Clearly state the invitation",
|
||||
"Provide full event details (What, When, Where)",
|
||||
"Explain the significance or purpose (optional)",
|
||||
"Mention who else might be there (optional)",
|
||||
"Request RSVP (date and contact method)",
|
||||
"Express anticipation",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be clear and specific about the details (what, when, where, why). Make it easy for the person to respond."
|
||||
},
|
||||
"friendship": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express appreciation for the friendship",
|
||||
"Share a recent memory or anecdote",
|
||||
"Acknowledge the value of the relationship",
|
||||
"Check in on them or share updates",
|
||||
"Look to the future (getting together, etc.)",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, personal, and specific about what you value in the friendship. Share updates and show genuine interest."
|
||||
},
|
||||
"love": {
|
||||
"structure": [
|
||||
"Greeting (Terms of endearment)",
|
||||
"Express depth of feelings",
|
||||
"Share a cherished memory or moment",
|
||||
"Describe specific qualities you love and appreciate",
|
||||
"Reaffirm commitment or future hopes",
|
||||
"Closing (Terms of endearment)"
|
||||
],
|
||||
"guidance": "Be sincere, personal, and specific about your feelings. Use sensory details and emotional language appropriate for your relationship."
|
||||
},
|
||||
"encouragement": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Acknowledge the situation or challenge they face",
|
||||
"Express belief in their abilities/strength",
|
||||
"Offer specific words of encouragement or support",
|
||||
"Remind them of past successes (optional)",
|
||||
"Offer practical help (optional)",
|
||||
"Look to the future with hope",
|
||||
"Closing with support"
|
||||
],
|
||||
"guidance": "Be positive, supportive, and specific about the person's strengths and abilities. Offer genuine encouragement and belief in them."
|
||||
},
|
||||
"farewell": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"State the purpose (saying goodbye)",
|
||||
"Express feelings about their departure (sadness, happiness for them)",
|
||||
"Share a positive memory or highlight their contribution",
|
||||
"Express good wishes for their future endeavors",
|
||||
"Look to staying in touch (optional)",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, reflective, and forward-looking. Focus on positive memories and express genuine good wishes for their next steps."
|
||||
},
|
||||
# Default personal letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Introduction",
|
||||
"Main content paragraphs",
|
||||
"Closing thoughts",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be personal, authentic, and appropriate for your relationship with the recipient. The tone is typically informal to semi-formal."
|
||||
}
|
||||
},
|
||||
"formal": {
|
||||
"application": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (if known)",
|
||||
"Subject line (Clear and concise)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State position applied for and where you saw it)",
|
||||
"Body paragraphs (Highlight relevant skills and experience)",
|
||||
"Closing paragraph (Reiterate interest, mention enclosed resume, call to action)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)",
|
||||
"Enclosures (Mention if attaching resume/portfolio)"
|
||||
],
|
||||
"guidance": "Be professional, concise, and specific about your qualifications and genuine interest in the position. Tailor it to the specific job description."
|
||||
},
|
||||
"complaint": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Clearly state it's a complaint)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the purpose: complaint about X service/product)",
|
||||
"Problem description (Provide specific details: date, time, location, product details, names if applicable)",
|
||||
"Impact statement (Explain how the problem affected you)",
|
||||
"Requested resolution (Clearly state what you want: refund, replacement, action)",
|
||||
"Closing paragraph (Reference attached documents, state expectation for response)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be clear, factual, and specific about the issue and your desired resolution. Maintain a respectful but firm tone. Include all relevant details."
|
||||
},
|
||||
"request": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Clearly state the request)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the purpose: making a request)",
|
||||
"Request details (Clearly explain what you are requesting)",
|
||||
"Justification (Explain why the request is necessary or beneficial)",
|
||||
"Provide supporting information (optional)",
|
||||
"Closing paragraph (Express gratitude for consideration, reiterate call to action)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and courteous about your request. Explain why it's important or beneficial to the recipient or organization."
|
||||
},
|
||||
"recommendation": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Letter of Recommendation for [Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your name, title, relationship to the recommendee, and for what purpose the letter is written)",
|
||||
"Body paragraphs (Describe the recommendee's qualifications, skills, and achievements with specific examples)",
|
||||
"Highlight relevant experiences and contributions",
|
||||
"Closing recommendation (Summarize endorsement, strongly recommend the person)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be specific, positive, and credible. Use concrete examples and anecdotes to support your recommendation. Tailor it to the specific role/opportunity."
|
||||
},
|
||||
"resignation": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Immediate supervisor/HR)",
|
||||
"Subject line (Letter of Resignation - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"Statement of resignation (Clearly state you are resigning)",
|
||||
"Last day of employment (Specify the date)",
|
||||
"Gratitude and reflection (Optional: Express thanks for the opportunity/experience)",
|
||||
"Transition plan/Offer of assistance (Optional: Suggest how to ensure a smooth handover)",
|
||||
"Closing paragraph (Express good wishes for the company's future)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be professional, positive (if possible), and clear about your departure and last day. Maintain a good relationship."
|
||||
},
|
||||
"inquiry": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Clearly state the nature of the inquiry)",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your purpose for writing - making an inquiry)",
|
||||
"Inquiry details (Provide necessary context or background)",
|
||||
"Specific questions (List your questions clearly, perhaps numbered)",
|
||||
"Closing paragraph (Express gratitude for assistance, indicate when you need a response)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and courteous about your inquiry. Organize your questions logically for easy answering."
|
||||
},
|
||||
"authorization": {
|
||||
"structure": [
|
||||
"Sender's contact information (The grantor of authority)",
|
||||
"Date",
|
||||
"Recipient's contact information (The person/entity receiving the letter)",
|
||||
"Subject line (Letter of Authorization)",
|
||||
"Salutation (Formal)",
|
||||
"Statement of authorization (Clearly state who is authorized)",
|
||||
"Authorized person's details (Full name, ID if applicable)",
|
||||
"Scope of authority (Precisely define what they are authorized to do)",
|
||||
"Limitations (Specify any restrictions or conditions)",
|
||||
"Duration of authorization (Start and end dates, if applicable)",
|
||||
"Closing paragraph (State responsibility, express confidence)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and precise about who is authorized, what they can do, for how long, and under what conditions. This is a legal document."
|
||||
},
|
||||
"appeal": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Appeals committee/relevant authority)",
|
||||
"Subject line (Letter of Appeal - [Your Name] - [Subject of Appeal])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your name, the decision being appealed, and the date of the decision)",
|
||||
"Grounds for appeal (Clearly state the reasons why you believe the decision is incorrect)",
|
||||
"Provide supporting evidence (Reference attached documents: records, photos, etc.)",
|
||||
"Explain mitigating circumstances (Optional)",
|
||||
"Requested outcome (Clearly state what resolution you seek)",
|
||||
"Closing paragraph (Express hope for reconsideration, gratitude for time)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be respectful, factual, and persuasive. Focus on valid grounds for appeal and provide clear, supporting evidence. Maintain a formal tone."
|
||||
},
|
||||
"introduction": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Introduction - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (Introduce yourself and the purpose of the letter)",
|
||||
"Background information (Briefly describe your relevant background or expertise)",
|
||||
"Reason for reaching out (Explain why you are introducing yourself to this specific person/entity)",
|
||||
"Potential areas of collaboration or shared interest (Optional)",
|
||||
"Call to action (Suggest a meeting, call, or further communication)",
|
||||
"Closing paragraph (Express enthusiasm for potential connection)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be professional, informative, and engaging. Clearly explain who you are, your expertise, and why you're reaching out to them specifically."
|
||||
},
|
||||
# Default formal letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Sender's address",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be professional, clear, and concise. Use formal language and structure. The tone is typically formal."
|
||||
}
|
||||
},
|
||||
"business": {
|
||||
"sales": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Benefit-oriented)",
|
||||
"Salutation",
|
||||
"Attention-grabbing opening (Address a pain point or introduce a benefit)",
|
||||
"Problem statement (Briefly describe the challenge the recipient faces)",
|
||||
"Solution presentation (Introduce your product/service as the solution)",
|
||||
"Benefits and features (Explain how your solution helps, focusing on benefits)",
|
||||
"Social proof (Optional: Testimonials, case studies, data)",
|
||||
"Call to action (Clearly state what you want them to do next)",
|
||||
"Closing paragraph (Reiterate benefit, create urgency/incentive)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)",
|
||||
"Enclosures (Optional: Brochure, pricing)"
|
||||
],
|
||||
"guidance": "Be persuasive, customer-focused, and clear about the value proposition. Focus on benefits, not just features. Make the call to action obvious."
|
||||
},
|
||||
"proposal": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Clear and descriptive)",
|
||||
"Salutation",
|
||||
"Introduction (State purpose: submitting a proposal)",
|
||||
"Problem statement/Needs assessment (Demonstrate understanding of client's needs)",
|
||||
"Proposed solution (Describe your solution in detail)",
|
||||
"Implementation plan (Outline steps and timeline)",
|
||||
"Costs and investment (Clearly state pricing and payment terms)",
|
||||
"Benefits and ROI (Explain the value the client will receive)",
|
||||
"Call to action (Suggest next steps: meeting, discussion)",
|
||||
"Closing paragraph (Express enthusiasm, availability for questions)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)",
|
||||
"Enclosures (Proposal document, appendix)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and persuasive about your solution. Focus on the client's needs and the value you provide. Structure it logically."
|
||||
},
|
||||
"order": {
|
||||
"structure": [
|
||||
"Letterhead (Your company)",
|
||||
"Date",
|
||||
"Recipient's address (Supplier)",
|
||||
"Subject line (Purchase Order - [PO Number])",
|
||||
"Salutation",
|
||||
"Introduction (Reference quote/agreement, state purpose: placing an order)",
|
||||
"Order details (Item list with quantities, descriptions, unit prices, total)",
|
||||
"Delivery requirements (Shipping address, requested delivery date, shipping method)",
|
||||
"Payment terms (Reference agreed terms)",
|
||||
"Closing paragraph (Express expectation for timely delivery)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and detailed about what you're ordering, quantities, delivery requirements, and payment terms. Include a purchase order number."
|
||||
},
|
||||
"quotation": {
|
||||
"structure": [
|
||||
"Letterhead (Your company)",
|
||||
"Date",
|
||||
"Recipient's address (Customer)",
|
||||
"Subject line (Quotation for [Product/Service])",
|
||||
"Salutation",
|
||||
"Introduction (Reference inquiry, state purpose: providing a quotation)",
|
||||
"Quotation details (List items/services, descriptions, unit prices, quantities, line totals)",
|
||||
"Pricing breakdown (Mention taxes, discounts, fees separately)",
|
||||
"Terms and conditions (Payment terms, delivery terms, warranty)",
|
||||
"Validity period (State how long the quote is valid)",
|
||||
"Next steps (How they can place an order)",
|
||||
"Closing paragraph (Express hope to do business, offer further assistance)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and transparent about pricing, terms, and what's included or excluded. Make it easy for the customer to understand and accept."
|
||||
},
|
||||
"acknowledgment": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Acknowledgment of [Received Item/Request])",
|
||||
"Salutation",
|
||||
"Acknowledgment statement (Clearly state what you have received or are acknowledging)",
|
||||
"Details of what's being acknowledged (Reference number, date, brief description)",
|
||||
"Confirm understanding (Optional: Briefly restate the request/issue to show understanding)",
|
||||
"Next steps (Outline what will happen next, e.g., processing order, investigating issue)",
|
||||
"Timeline (Provide an estimated timeframe if possible)",
|
||||
"Closing paragraph (Express gratitude, offer further assistance)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be prompt, clear, and specific about what you're acknowledging. Set clear expectations for next steps and timelines."
|
||||
},
|
||||
"collection": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Invoice [Invoice Number] - Payment Due)",
|
||||
"Salutation",
|
||||
"Introduction (Reference invoice number and due date)",
|
||||
"Account status (Clearly state the outstanding amount)",
|
||||
"Payment request (Politely request payment)",
|
||||
"Payment options (Remind them how to pay)",
|
||||
"Consequences of non-payment (Optional: Briefly mention late fees or further action, depending on letter stage)",
|
||||
"Call to action (Request payment by a specific date)",
|
||||
"Closing paragraph (Express hope for prompt payment, offer to discuss)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be firm but professional. Clearly state the amount due, due date, and payment options. The tone may vary depending on how overdue the payment is."
|
||||
},
|
||||
"adjustment": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address (Customer who made a complaint)",
|
||||
"Subject line (Response to your inquiry - [Reference Number])",
|
||||
"Salutation",
|
||||
"Acknowledgment of complaint (Reference their communication and the issue)",
|
||||
"Investigation findings (Explain the outcome of your investigation)",
|
||||
"Adjustment offered (Clearly state the resolution: refund, replacement, credit, etc.)",
|
||||
"Apology (Optional: Express regret for the inconvenience)",
|
||||
"Preventive measures (Optional: Explain steps taken to prevent recurrence)",
|
||||
"Closing paragraph (Express hope for continued business, offer further assistance)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be responsive, empathetic, and solution-oriented. Clearly explain the adjustment and any preventive measures taken."
|
||||
},
|
||||
"credit": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address (Applicant)",
|
||||
"Subject line (Credit Application Status - [Applicant Name])",
|
||||
"Salutation",
|
||||
"Introduction (Reference their credit application and the purpose of the letter)",
|
||||
"Credit decision (Clearly state if credit is approved or denied)",
|
||||
"If approved: Credit terms (Credit limit, payment terms, interest rates)",
|
||||
"If denied: Reason for decision (Provide specific, compliant reasons)",
|
||||
"Requirements (If approved: any further steps or documents needed)",
|
||||
"Closing paragraph (If approved: Express welcome; If denied: Offer alternative options or appeals process)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and transparent about the credit decision, terms, limits, or reasons for denial. Ensure compliance with regulations if denying credit."
|
||||
},
|
||||
"follow_up": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line (Following up on [Previous Communication/Meeting])",
|
||||
"Salutation",
|
||||
"Reference to previous communication (Mention date, topic, or meeting)",
|
||||
"Purpose of follow-up (Clearly state why you are writing again)",
|
||||
"Action items/Next steps (Remind of agreed-upon actions or propose next steps)",
|
||||
"Provide additional information (Optional)",
|
||||
"Call to action (If applicable, e.g., request a response, schedule a meeting)",
|
||||
"Closing paragraph (Reiterate interest, express anticipation)",
|
||||
"Complimentary close (Professional)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be clear, specific, and action-oriented. Reference previous communication and clearly state the purpose of your follow-up and desired outcome."
|
||||
},
|
||||
# Default business letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Letterhead",
|
||||
"Date",
|
||||
"Recipient's address",
|
||||
"Subject line",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be professional, clear, and concise. Focus on the business purpose of your letter. The tone is typically formal to semi-formal."
|
||||
}
|
||||
},
|
||||
"cover": {
|
||||
"standard": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information (if known)",
|
||||
"Subject line (Job Application - [Your Name] - [Job Title])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the position you are applying for, where you saw the advertisement, and a brief statement of enthusiasm)",
|
||||
"Body paragraph 1 (Highlight skills and experience directly relevant to the job description - often 1-2 key qualifications)",
|
||||
"Body paragraph 2 (Provide a specific example or anecdote demonstrating your abilities)",
|
||||
"Body paragraph 3 (Connect your passion/goals to the company's mission/values - optional but effective)",
|
||||
"Closing paragraph (Reiterate interest, mention attached resume, express availability for interview)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Be professional, specific about your most relevant qualifications, and clear about your interest in the position. Tailor every cover letter to the specific job and company."
|
||||
},
|
||||
"career_change": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Job Application - [Your Name] - [Job Title])",
|
||||
"Salutation",
|
||||
"Introduction (State the position and acknowledge your career transition)",
|
||||
"Body paragraph 1 (Highlight transferable skills from previous roles)",
|
||||
"Body paragraph 2 (Explain your motivation for the career change and how your skills apply)",
|
||||
"Body paragraph 3 (Demonstrate understanding of the new industry/role)",
|
||||
"Closing paragraph (Reiterate enthusiasm, mention enclosed resume, call to action)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Focus on transferable skills and explain your career transition. Connect your past experience and new skills directly to the requirements of the target role."
|
||||
},
|
||||
"entry_level": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Job Application - [Your Name] - [Job Title])",
|
||||
"Salutation",
|
||||
"Introduction (State the position and your enthusiasm for the opportunity as a recent graduate/entrant)",
|
||||
"Body paragraph 1 (Highlight relevant education, coursework, GPA if strong)",
|
||||
"Body paragraph 2 (Describe relevant internships, projects, or volunteer experience)",
|
||||
"Body paragraph 3 (Showcase soft skills: teamwork, communication, eagerness to learn)",
|
||||
"Closing paragraph (Reiterate interest, mention attached resume, express availability for interview)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Emphasize education, relevant internships/projects, and transferable skills gained through academic or extracurricular activities. Show strong potential and enthusiasm."
|
||||
},
|
||||
"executive": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Senior Executive/Board Member)",
|
||||
"Subject line (Executive Application - [Your Name] - [Position])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State position applying for, brief summary of executive profile)",
|
||||
"Body paragraph 1 (Highlight strategic leadership experience and key achievements)",
|
||||
"Body paragraph 2 (Discuss relevant industry expertise and market insights)",
|
||||
"Body paragraph 3 (Describe experience in driving growth, managing teams, achieving results)",
|
||||
"Closing paragraph (Reiterate interest, express desire to discuss contribution to the organization)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Emphasize strategic leadership experience, significant achievements with measurable results, and industry expertise. Use a confident, authoritative, and forward-looking tone."
|
||||
},
|
||||
"creative": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Application - [Your Name] - [Creative Role])",
|
||||
"Salutation",
|
||||
"Creative introduction (Engaging hook related to the role or your passion)",
|
||||
"Body paragraph 1 (Highlight relevant creative experience and skills)",
|
||||
"Body paragraph 2 (Reference specific portfolio pieces or projects that showcase your style/abilities)",
|
||||
"Body paragraph 3 (Describe your creative process or approach)",
|
||||
"Closing paragraph (Reiterate enthusiasm, mention attached resume/portfolio link, call to action)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Use a more engaging and expressive style appropriate for a creative role while maintaining professionalism. Highlight specific creative achievements and link to your portfolio."
|
||||
},
|
||||
"technical": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Application - [Your Name] - [Technical Role])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State position, source, and brief technical interest)",
|
||||
"Body paragraph 1 (Highlight specific technical skills and proficiencies relevant to the job description)",
|
||||
"Body paragraph 2 (Describe relevant technical projects or challenges you've solved)",
|
||||
"Body paragraph 3 (Discuss problem-solving abilities and experience with relevant technologies)",
|
||||
"Closing paragraph (Reiterate interest, mention attached resume, express availability for technical discussion/interview)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Focus on technical skills, relevant projects, and problem-solving abilities. Use appropriate technical terminology accurately."
|
||||
},
|
||||
"academic": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (Search Committee Chair)",
|
||||
"Subject line (Application for [Position] - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State the position, the department, and express your strong interest)",
|
||||
"Body paragraph 1 (Discuss your research experience, focus on key projects and contributions)",
|
||||
"Body paragraph 2 (Describe your teaching philosophy and relevant teaching experience)",
|
||||
"Body paragraph 3 (Mention publications, presentations, grants, and other scholarly contributions)",
|
||||
"Closing paragraph (Reiterate enthusiasm for joining the faculty, express availability for interview/presentation)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name)"
|
||||
],
|
||||
"guidance": "Focus on research experience, teaching philosophy, publications, and contributions to the field. Use a scholarly and professional tone suitable for academia."
|
||||
},
|
||||
"remote": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Remote Application - [Your Name] - [Job Title])",
|
||||
"Salutation",
|
||||
"Introduction (State the remote position, source, and enthusiasm for remote work)",
|
||||
"Body paragraph 1 (Highlight experience working remotely or independently)",
|
||||
"Body paragraph 2 (Emphasize self-management, time management, and organizational skills required for remote work)",
|
||||
"Body paragraph 3 (Describe strong written and verbal communication skills, essential for remote collaboration)",
|
||||
"Closing paragraph (Reiterate interest in the remote role, mention attached resume, express availability for video interview)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Emphasize self-motivation, excellent communication skills (especially written), time management, and any prior experience working independently or in remote teams."
|
||||
},
|
||||
"referral": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Hiring Manager contact information",
|
||||
"Subject line (Referral Application - [Your Name] - [Job Title] - Referred by [Referrer's Name])",
|
||||
"Salutation",
|
||||
"Referral introduction (Immediately state who referred you and for what position)",
|
||||
"Body paragraph 1 (Briefly explain your connection to the referrer and how you learned about the role)",
|
||||
"Body paragraph 2 (Highlight key qualifications relevant to the job description)",
|
||||
"Body paragraph 3 (Express strong interest in the position and the company)",
|
||||
"Closing paragraph (Reiterate enthusiasm, mention attached resume, express availability for interview)",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Mention the referral prominently and early. Explain your connection to the referrer and how it aligns with your interest in the role. Still, ensure you highlight your own qualifications."
|
||||
},
|
||||
# Default cover letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Contact information",
|
||||
"Date",
|
||||
"Recipient's information",
|
||||
"Subject line",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be professional, specific about your qualifications, and clear about your interest in the position. Tailor your letter to the specific job and company."
|
||||
}
|
||||
},
|
||||
"recommendation": {
|
||||
# Recommendation letters are often considered a subtype of Formal,
|
||||
# but can be a top-level type in some systems. Keeping the structure
|
||||
# consistent with the original request, but noting this potential overlap.
|
||||
"standard": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information (e.g., Admissions Committee, Hiring Manager)",
|
||||
"Subject line (Letter of Recommendation for [Name])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State your name, title, relationship to the recommendee, how long you've known them, and for what opportunity the letter is written)",
|
||||
"Body paragraph 1 (Describe their relevant skills and qualities, providing specific examples)",
|
||||
"Body paragraph 2 (Discuss their achievements or contributions, with context and impact)",
|
||||
"Body paragraph 3 (Optional: Mention character traits, teamwork, or specific anecdotes)",
|
||||
"Overall Endorsement (Summarize your strong recommendation and why they are a good fit)",
|
||||
"Closing paragraph (Offer to provide further information)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Typed name and title)"
|
||||
],
|
||||
"guidance": "Be specific, positive, and credible. Use concrete examples and anecdotes to support your recommendation. Clearly state your relationship with the person and for what opportunity you are recommending them."
|
||||
},
|
||||
# Default recommendation letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Letter of Recommendation for [Name])",
|
||||
"Salutation",
|
||||
"Introduction",
|
||||
"Body paragraphs describing qualifications and experiences",
|
||||
"Specific examples and anecdotes",
|
||||
"Overall endorsement and recommendation",
|
||||
"Closing and offer for further information",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Provide a strong, positive, and specific endorsement based on your professional or academic relationship with the individual."
|
||||
}
|
||||
},
|
||||
"complaint": {
|
||||
# Complaint letters are often considered a subtype of Formal or Business,
|
||||
# but can be a top-level type. Keeping the structure consistent.
|
||||
"product": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Company contact information",
|
||||
"Subject line (Complaint Regarding [Product Name/Model])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State purpose: complaining about a product, include product name, model, date/place of purchase)",
|
||||
"Problem description (Explain the specific defect or issue with the product in detail)",
|
||||
"History of the problem (Mention if you've tried fixing it, contacted support, etc.)",
|
||||
"Desired resolution (Clearly state if you want a refund, replacement, repair)",
|
||||
"Call to action (State what you expect the company to do and by when)",
|
||||
"Closing paragraph (Reference attached documents like receipt, express expectation for resolution)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be clear, factual, and specific about the product issue and your desired resolution. Include all relevant details like model number, date of purchase, and copies of receipts. Maintain a firm but professional tone."
|
||||
},
|
||||
"service": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Company/Service Provider contact information",
|
||||
"Subject line (Complaint Regarding [Service Type/Issue])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State purpose: complaining about a service received, include date/time/location of service)",
|
||||
"Problem description (Explain the specific issue with the service provided in detail)",
|
||||
"Impact of the issue (Explain how this problem affected you)",
|
||||
"Desired resolution (Clearly state what you want: refund, re-performance of service, compensation)",
|
||||
"Call to action (State what you expect the company to do and by when)",
|
||||
"Closing paragraph (Reference any relevant documents, express expectation for resolution)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be clear, factual, and specific about the service issue and your desired resolution. Include details like dates, times, and names of service providers if possible. Maintain a firm but professional tone."
|
||||
},
|
||||
"billing": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Company contact information",
|
||||
"Subject line (Complaint Regarding Billing Error - Account #[Your Account Number])",
|
||||
"Salutation (Formal)",
|
||||
"Introduction (State purpose: complaining about a billing error, include account number and invoice number)",
|
||||
"Problem description (Explain the specific error on the bill: incorrect charge, double billing, etc.)",
|
||||
"Provide supporting evidence (Reference payments made, attach relevant statements)",
|
||||
"Desired resolution (Clearly state what you want: correction of bill, refund, credit)",
|
||||
"Call to action (State what you expect the company to do and by when)",
|
||||
"Closing paragraph (Reference attached documents, express expectation for resolution)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be clear, factual, and specific about the billing error. Provide supporting documentation like invoices or payment records. Clearly state the desired correction."
|
||||
},
|
||||
# Default complaint letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Complaint Regarding [Issue Summary])",
|
||||
"Salutation",
|
||||
"Introduction (State the purpose of the letter - to complain)",
|
||||
"Detailed description of the problem",
|
||||
"Explanation of the impact",
|
||||
"Desired resolution",
|
||||
"Call to action",
|
||||
"Closing",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be clear, factual, and specific about the issue and your desired resolution. Maintain a respectful but firm tone and provide relevant details."
|
||||
}
|
||||
},
|
||||
"thank_you": {
|
||||
# Thank You letters are often considered a subtype of Personal or Business,
|
||||
# but can be a top-level type. Keeping the structure consistent.
|
||||
"personal": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express gratitude clearly and sincerely",
|
||||
"Specify what you are thankful for (gift, favor, support)",
|
||||
"Explain the impact it had on you or how you used it",
|
||||
"Share a personal thought or memory related to it (optional)",
|
||||
"Look to the future or express continued appreciation",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be warm, sincere, and specific about what you are thankful for. Personalize the message and explain the impact of their action or gift."
|
||||
},
|
||||
"professional": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Thank You - [Your Name])",
|
||||
"Salutation (Formal/Semi-formal)",
|
||||
"Express gratitude clearly (e.g., Thank you for the interview, thank you for your help)",
|
||||
"Specify what you are thankful for (Meeting date/topic, specific assistance)",
|
||||
"Reiterate interest or connection (e.g., Reiterate interest in the job, mention something discussed)",
|
||||
"Express appreciation for their time or effort",
|
||||
"Closing paragraph (Optional: look to future interaction)",
|
||||
"Complimentary close (Formal/Semi-formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be prompt, professional, and specific. Reiterate your interest or key points discussed. Send within 24 hours for interviews."
|
||||
},
|
||||
"after_interview": {
|
||||
"structure": [
|
||||
"Your contact information",
|
||||
"Date",
|
||||
"Interviewer's contact information",
|
||||
"Subject line (Thank You - [Your Name] - [Job Title])",
|
||||
"Salutation (Formal)",
|
||||
"Express sincere thanks for the interview opportunity",
|
||||
"Mention the specific position and date of the interview",
|
||||
"Reiterate your strong interest in the role and the company",
|
||||
"Reference a specific point discussed during the interview to show engagement",
|
||||
"Briefly highlight how your skills/experience align with a need discussed",
|
||||
"Express enthusiasm for next steps",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Send within 24 hours of the interview. Be specific, professional, and reiterate your key strengths and interest. Proofread carefully."
|
||||
},
|
||||
# Default thank you letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Express thanks",
|
||||
"Specify reason for thanks",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be sincere and specific about what you are thankful for."
|
||||
}
|
||||
},
|
||||
"invitation": {
|
||||
# Invitation letters are often considered a subtype of Personal or Formal,
|
||||
# but can be a top-level type. Keeping the structure consistent.
|
||||
"event": { # e.g., party, gathering, wedding
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"State the purpose: extending an invitation",
|
||||
"Event details (Type of event, Host)",
|
||||
"Date and Time",
|
||||
"Location (Full address)",
|
||||
"Purpose/Theme (Optional)",
|
||||
"Special instructions (Dress code, what to bring, etc. - optional)",
|
||||
"RSVP information (Date, Contact method)",
|
||||
"Express anticipation",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be clear about all the event details (What, When, Where). Make it easy for guests to RSVP. Tone can be formal or informal depending on the event."
|
||||
},
|
||||
"interview": {
|
||||
"structure": [
|
||||
"Company Letterhead",
|
||||
"Date",
|
||||
"Candidate's contact information",
|
||||
"Subject line (Interview Invitation - [Job Title] - [Your Name])",
|
||||
"Salutation (Formal)",
|
||||
"State the purpose: inviting them for an interview",
|
||||
"Specify the position applied for",
|
||||
"Propose date(s) and time(s) for the interview",
|
||||
"Provide location details (Address, or link for virtual)",
|
||||
"Mention who they will meet with (Names and titles)",
|
||||
"Explain the interview format/duration (Optional)",
|
||||
"Instructions (What to bring, who to contact with questions)",
|
||||
"Call to action (Request confirmation or scheduling)",
|
||||
"Closing paragraph (Express anticipation)",
|
||||
"Complimentary close (Formal)",
|
||||
"Signature (Interviewer/HR Contact Name and Title)"
|
||||
],
|
||||
"guidance": "Be professional, clear, and provide all necessary details for the candidate. Make the scheduling process straightforward."
|
||||
},
|
||||
"meeting": {
|
||||
"structure": [
|
||||
"Sender's contact information",
|
||||
"Date",
|
||||
"Recipient's contact information",
|
||||
"Subject line (Invitation to Meeting - [Meeting Topic])",
|
||||
"Salutation",
|
||||
"State the purpose: inviting them to a meeting",
|
||||
"Meeting details (Date, Time, Location/Virtual link)",
|
||||
"Purpose/Agenda (Clearly state what the meeting is about)",
|
||||
"Expected duration (Optional)",
|
||||
"Preparation required (Optional: Documents to review)",
|
||||
"RSVP information (Optional)",
|
||||
"Closing paragraph",
|
||||
"Complimentary close",
|
||||
"Signature"
|
||||
],
|
||||
"guidance": "Be clear about the purpose, date, time, and location. Provide an agenda so attendees can prepare. The tone can be formal or informal depending on the context."
|
||||
},
|
||||
# Default invitation letter template if subtype is not found
|
||||
"default": {
|
||||
"structure": [
|
||||
"Greeting",
|
||||
"Invitation statement",
|
||||
"Event/Meeting details (What, When, Where)",
|
||||
"Purpose (Optional)",
|
||||
"RSVP information",
|
||||
"Closing"
|
||||
],
|
||||
"guidance": "Be clear and specific about the details of the event or meeting."
|
||||
}
|
||||
},
|
||||
# Overall default template if letter type is not recognized
|
||||
"default": {
|
||||
"structure": [
|
||||
"Introduction",
|
||||
"Body paragraphs",
|
||||
"Conclusion"
|
||||
],
|
||||
"guidance": "Be clear, concise, and appropriate for your audience and purpose. This is a generic structure."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_template_by_type(letter_type: str, subtype: str = "default") -> Dict[str, Any]:
|
||||
"""
|
||||
Get a template for a specific letter type and subtype using a dictionary lookup.
|
||||
|
||||
Args:
|
||||
letter_type: Type of letter (e.g., "personal", "formal", "business", "cover").
|
||||
subtype: Subtype of letter (e.g., "congratulations", "application", "sales").
|
||||
Defaults to "default" if no subtype is specified.
|
||||
|
||||
Returns:
|
||||
Template dictionary with 'structure' (List[str]) and 'guidance' (str).
|
||||
Returns the default template if the letter type or subtype is not found.
|
||||
"""
|
||||
# Get templates for the specific letter type, or the overall default templates
|
||||
type_templates = TEMPLATES.get(letter_type, TEMPLATES["default"])
|
||||
|
||||
# Get the template for the specific subtype, or the default for that letter type
|
||||
template = type_templates.get(subtype, type_templates.get("default", TEMPLATES["default"])) # Fallback to overall default
|
||||
|
||||
# Ensure the returned template always has 'structure' and 'guidance' keys
|
||||
# This handles cases where an incomplete template might have been defined (error tolerance)
|
||||
if "structure" not in template or not isinstance(template["structure"], list):
|
||||
template["structure"] = ["Introduction", "Body", "Conclusion"]
|
||||
template["guidance"] = "Generic template: structure or guidance missing."
|
||||
|
||||
if "guidance" not in template or not isinstance(template["guidance"], str):
|
||||
template["guidance"] = "Generic guidance: structure or guidance missing."
|
||||
|
||||
|
||||
return template
|
||||
|
||||
# Example usage (for testing purposes)
|
||||
if __name__ == '__main__':
|
||||
# Test cases
|
||||
print("--- Testing Letter Templates ---")
|
||||
|
||||
personal_congrats = get_template_by_type("personal", "congratulations")
|
||||
print("\nPersonal Congratulations Template:")
|
||||
print(f"Structure: {personal_congrats['structure']}")
|
||||
print(f"Guidance: {personal_congrats['guidance']}")
|
||||
|
||||
formal_complaint = get_template_by_type("formal", "complaint")
|
||||
print("\nFormal Complaint Template:")
|
||||
print(f"Structure: {formal_complaint['structure']}")
|
||||
print(f"Guidance: {formal_complaint['guidance']}")
|
||||
|
||||
business_sales = get_template_by_type("business", "sales")
|
||||
print("\nBusiness Sales Template:")
|
||||
print(f"Structure: {business_sales['structure']}")
|
||||
print(f"Guidance: {business_sales['guidance']}")
|
||||
|
||||
cover_entry_level = get_template_by_type("cover", "entry_level")
|
||||
print("\nCover Entry Level Template:")
|
||||
print(f"Structure: {cover_entry_level['structure']}")
|
||||
print(f"Guidance: {cover_entry_level['guidance']}")
|
||||
|
||||
unknown_type = get_template_by_type("unknown_type", "some_subtype")
|
||||
print("\nUnknown Type Template (Should be Default):")
|
||||
print(f"Structure: {unknown_type['structure']}")
|
||||
print(f"Guidance: {unknown_type['guidance']}")
|
||||
|
||||
personal_unknown_subtype = get_template_by_type("personal", "unknown_subtype")
|
||||
print("\nPersonal Unknown Subtype Template (Should be Personal Default):")
|
||||
print(f"Structure: {personal_unknown_subtype['structure']}")
|
||||
print(f"Guidance: {personal_unknown_subtype['guidance']}")
|
||||
@@ -1,557 +0,0 @@
|
||||
# Blog Outline Generator
|
||||
|
||||
A powerful AI-powered tool for generating comprehensive blog outlines with advanced editing capabilities, content generation, and image integration.
|
||||
|
||||
## 🛠 Technical Architecture
|
||||
|
||||
### Core Components
|
||||
- **Backend**: Python-based implementation using Streamlit for UI
|
||||
- **AI Integration**:
|
||||
- Text Generation: Integration with multiple LLM providers (Gemini, OpenAI, Anthropic)
|
||||
- Image Generation: Support for multiple image generation APIs (Gemini-AI, Dalle3, Stability-AI)
|
||||
- **Data Structures**:
|
||||
```python
|
||||
class OutlineConfig:
|
||||
content_type: ContentType
|
||||
content_depth: ContentDepth
|
||||
outline_style: OutlineStyle
|
||||
target_word_count: int
|
||||
num_main_sections: int
|
||||
num_subsections_per_section: int
|
||||
include_images: bool
|
||||
image_style: str
|
||||
image_engine: str
|
||||
```
|
||||
|
||||
### Key Technologies
|
||||
- **Streamlit**: Web application framework
|
||||
- **Asyncio**: Asynchronous operations for AI calls
|
||||
- **Loguru**: Advanced logging system
|
||||
- **BeautifulSoup**: Web content parsing
|
||||
- **Pydantic**: Data validation
|
||||
- **Markdown**: Content formatting
|
||||
|
||||
## 🌟 Features with Examples
|
||||
|
||||
### 1. Content Generation
|
||||
- **AI-Powered Content Creation**:
|
||||
```python
|
||||
# Example prompt for content generation
|
||||
prompt = f"""
|
||||
Generate content for a {content_type} article about {topic}.
|
||||
Target audience: {target_audience}
|
||||
Word count: {target_word_count}
|
||||
Style: {outline_style}
|
||||
"""
|
||||
content = await llm_text_gen(prompt)
|
||||
```
|
||||
|
||||
- **Multiple Content Types**:
|
||||
```python
|
||||
# Example configuration for different content types
|
||||
config = OutlineConfig(
|
||||
content_type=ContentType.TUTORIAL,
|
||||
content_depth=ContentDepth.INTERMEDIATE,
|
||||
target_word_count=2000
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Outline Structure
|
||||
- **Flexible Section Management**:
|
||||
```python
|
||||
# Example section generation
|
||||
async def generate_sections(self, topic: str) -> List[str]:
|
||||
sections = []
|
||||
for i in range(self.config.num_main_sections):
|
||||
section = await self._generate_section(topic, i)
|
||||
sections.append(section)
|
||||
return sections
|
||||
```
|
||||
|
||||
- **Optional Components**:
|
||||
```python
|
||||
# Example FAQ generation
|
||||
async def generate_faqs(self, topic: str) -> List[str]:
|
||||
prompt = f"""
|
||||
Generate 5 common questions about {topic}
|
||||
Content type: {self.config.content_type}
|
||||
Target audience: {self.config.target_audience}
|
||||
"""
|
||||
return await llm_text_gen(prompt)
|
||||
```
|
||||
|
||||
### 3. Advanced Editing Capabilities
|
||||
- **Section Content Editor**:
|
||||
```python
|
||||
# Example content editing interface
|
||||
def edit_section_content(self, section: str, content: str) -> str:
|
||||
edited_content = st.text_area(
|
||||
"Edit Content",
|
||||
value=content,
|
||||
height=300,
|
||||
key=f"content_edit_{section}"
|
||||
)
|
||||
return edited_content
|
||||
```
|
||||
|
||||
- **Subsection Management**:
|
||||
```python
|
||||
# Example subsection reordering
|
||||
def reorder_subsections(self, section: str, subsections: List[str]) -> List[str]:
|
||||
for i, subsection in enumerate(subsections):
|
||||
if st.button("↑", key=f"move_up_{section}_{i}"):
|
||||
subsections[i], subsections[i-1] = subsections[i-1], subsections[i]
|
||||
return subsections
|
||||
```
|
||||
|
||||
### 4. Image Generation
|
||||
- **AI Image Generation**:
|
||||
```python
|
||||
# Example image generation
|
||||
async def generate_image(self, prompt: str, style: str) -> str:
|
||||
image_prompt = f"""
|
||||
Create a {style} image for: {prompt}
|
||||
Style: {self.config.image_style}
|
||||
"""
|
||||
return await generate_image(image_prompt)
|
||||
```
|
||||
|
||||
### 5. Content Optimization
|
||||
- **SEO Features**:
|
||||
```python
|
||||
# Example SEO optimization
|
||||
def optimize_content(self, content: str, keywords: List[str]) -> str:
|
||||
for keyword in keywords:
|
||||
content = self._naturally_insert_keyword(content, keyword)
|
||||
return content
|
||||
```
|
||||
|
||||
## 📊 Technical Implementation Details
|
||||
|
||||
### 1. Content Generation Pipeline
|
||||
```python
|
||||
async def generate_content(self, topic: str) -> Dict:
|
||||
# 1. Generate outline structure
|
||||
outline = await self.generate_outline(topic)
|
||||
|
||||
# 2. Generate content for each section
|
||||
for section in outline:
|
||||
content = await self.generate_section_content(section)
|
||||
outline[section]['content'] = content
|
||||
|
||||
# 3. Generate images if enabled
|
||||
if self.config.include_images:
|
||||
for section in outline:
|
||||
image = await self.generate_section_image(section)
|
||||
outline[section]['image'] = image
|
||||
|
||||
return outline
|
||||
```
|
||||
|
||||
### 2. AI Integration
|
||||
```python
|
||||
class AIIntegration:
|
||||
def __init__(self, provider: str):
|
||||
self.provider = provider
|
||||
self.model = self._initialize_model()
|
||||
|
||||
async def generate_text(self, prompt: str) -> str:
|
||||
if self.provider == "gemini":
|
||||
return await gemini_text_response(prompt)
|
||||
elif self.provider == "openai":
|
||||
return await openai_chatgpt(prompt)
|
||||
```
|
||||
|
||||
### 3. Image Processing
|
||||
```python
|
||||
class ImageProcessor:
|
||||
def __init__(self, engine: str):
|
||||
self.engine = engine
|
||||
|
||||
async def generate_image(self, prompt: str) -> str:
|
||||
if self.engine == "Gemini-AI":
|
||||
return await generate_gemini_image(prompt)
|
||||
elif self.engine == "Dalle3":
|
||||
return await generate_dalle3_images(prompt)
|
||||
```
|
||||
|
||||
## 🔧 Configuration Examples
|
||||
|
||||
### 1. Basic Configuration
|
||||
```python
|
||||
config = OutlineConfig(
|
||||
content_type=ContentType.GUIDE,
|
||||
content_depth=ContentDepth.INTERMEDIATE,
|
||||
target_word_count=2000,
|
||||
num_main_sections=5,
|
||||
num_subsections_per_section=3
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Advanced Configuration
|
||||
```python
|
||||
config = OutlineConfig(
|
||||
content_type=ContentType.TUTORIAL,
|
||||
content_depth=ContentDepth.ADVANCED,
|
||||
outline_style=OutlineStyle.MODERN,
|
||||
target_word_count=3000,
|
||||
include_images=True,
|
||||
image_style="realistic",
|
||||
image_engine="Gemini-AI",
|
||||
target_audience="developers",
|
||||
language="English",
|
||||
keywords=["python", "tutorial", "advanced"]
|
||||
)
|
||||
```
|
||||
|
||||
## 📝 Usage Examples
|
||||
|
||||
### 1. Basic Usage
|
||||
```python
|
||||
# Initialize generator
|
||||
generator = BlogOutlineGenerator()
|
||||
|
||||
# Generate outline
|
||||
outline = await generator.generate_outline("Python Programming Basics")
|
||||
|
||||
# Export to markdown
|
||||
markdown = generator.to_markdown()
|
||||
```
|
||||
|
||||
### 2. Advanced Usage
|
||||
```python
|
||||
# Custom configuration
|
||||
config = OutlineConfig(
|
||||
content_type=ContentType.TUTORIAL,
|
||||
content_depth=ContentDepth.ADVANCED,
|
||||
include_images=True
|
||||
)
|
||||
|
||||
# Initialize with config
|
||||
generator = BlogOutlineGenerator(config)
|
||||
|
||||
# Generate with custom settings
|
||||
outline = await generator.generate_outline(
|
||||
"Advanced Python Decorators",
|
||||
keywords=["python", "decorators", "advanced"]
|
||||
)
|
||||
|
||||
# Export to multiple formats
|
||||
markdown = generator.to_markdown()
|
||||
json_output = generator.to_json()
|
||||
html_output = generator.to_html()
|
||||
```
|
||||
|
||||
## 🔍 Technical Considerations
|
||||
|
||||
### 1. Performance Optimization
|
||||
- Asynchronous operations for AI calls
|
||||
- Caching of generated content
|
||||
- Batch processing for images
|
||||
- Memory management for large documents
|
||||
|
||||
### 2. Error Handling
|
||||
```python
|
||||
try:
|
||||
content = await llm_text_gen(prompt)
|
||||
except Exception as e:
|
||||
logger.error(f"Content generation failed: {e}")
|
||||
return None
|
||||
```
|
||||
|
||||
### 3. Data Validation
|
||||
```python
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
class SectionContent(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
image_path: Optional[str]
|
||||
|
||||
@validator('content')
|
||||
def validate_content_length(cls, v):
|
||||
if len(v.split()) < 100:
|
||||
raise ValueError("Content too short")
|
||||
return v
|
||||
```
|
||||
|
||||
## 🌟 Features
|
||||
|
||||
### 1. Content Generation
|
||||
- **AI-Powered Content Creation**: Generate high-quality content for each section using advanced language models
|
||||
- **Multiple Content Types**: Support for various content formats including:
|
||||
- How-to guides
|
||||
- Tutorials
|
||||
- Listicles
|
||||
- Comparisons
|
||||
- Case studies
|
||||
- Opinion pieces
|
||||
- News articles
|
||||
- Reviews
|
||||
- General guides
|
||||
- **Customizable Content Depth**:
|
||||
- Basic: Simple, easy-to-understand content
|
||||
- Intermediate: Balanced depth with practical examples
|
||||
- Advanced: Detailed technical content
|
||||
- Expert: In-depth analysis and advanced concepts
|
||||
|
||||
### 2. Outline Structure
|
||||
- **Flexible Section Management**:
|
||||
- Customizable number of main sections
|
||||
- Configurable subsections per section
|
||||
- Dynamic section reordering
|
||||
- Easy addition/removal of sections
|
||||
- **Optional Components**:
|
||||
- Introduction section
|
||||
- Conclusion section
|
||||
- FAQ section
|
||||
- Additional resources section
|
||||
|
||||
### 3. Advanced Editing Capabilities
|
||||
- **Section Content Editor**:
|
||||
- Rich text editing interface
|
||||
- Real-time word count tracking
|
||||
- Formatting options (Bold, Italic, Lists, Code Blocks, Links)
|
||||
- AI-powered content enhancement
|
||||
- **Subsection Management**:
|
||||
- Drag-and-drop reordering
|
||||
- Individual subsection editing
|
||||
- Add/remove subsection functionality
|
||||
- Bulk editing capabilities
|
||||
- **Metadata Editing**:
|
||||
- Section-specific settings
|
||||
- Content depth adjustment
|
||||
- Target word count configuration
|
||||
- Image settings customization
|
||||
|
||||
### 4. Image Generation
|
||||
- **AI Image Generation**:
|
||||
- Multiple image styles (realistic, illustration, minimalist, photographic, artistic)
|
||||
- Support for multiple image engines (Gemini-AI, Dalle3, Stability-AI)
|
||||
- Custom image prompts
|
||||
- Image regeneration capability
|
||||
- **Image Integration**:
|
||||
- Automatic image placement
|
||||
- Image preview and editing
|
||||
- Image prompt viewing and editing
|
||||
- Image style customization
|
||||
|
||||
### 5. Content Optimization
|
||||
- **SEO Features**:
|
||||
- Keyword integration
|
||||
- Content structure optimization
|
||||
- Meta description generation
|
||||
- SEO-friendly formatting
|
||||
- **Audience Targeting**:
|
||||
- Customizable target audience
|
||||
- Language selection
|
||||
- Content tone adjustment
|
||||
- Reading level optimization
|
||||
|
||||
### 6. Export Options
|
||||
- **Multiple Formats**:
|
||||
- Markdown export
|
||||
- JSON export
|
||||
- HTML export
|
||||
- Custom formatting options
|
||||
- **Download Capabilities**:
|
||||
- One-click download
|
||||
- Format-specific styling
|
||||
- Custom file naming
|
||||
- Batch export options
|
||||
|
||||
### 7. User Interface
|
||||
- **Intuitive Design**:
|
||||
- Clean, modern interface
|
||||
- Responsive layout
|
||||
- Easy navigation
|
||||
- Clear visual hierarchy
|
||||
- **Interactive Features**:
|
||||
- Real-time preview
|
||||
- Drag-and-drop functionality
|
||||
- Quick edit options
|
||||
- Contextual help
|
||||
|
||||
### 8. Statistics and Analytics
|
||||
- **Content Metrics**:
|
||||
- Word count tracking
|
||||
- Section statistics
|
||||
- Subsection counts
|
||||
- Content depth analysis
|
||||
- **Progress Tracking**:
|
||||
- Generation progress
|
||||
- Edit history
|
||||
- Version comparison
|
||||
- Performance metrics
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Usage
|
||||
1. Launch the application:
|
||||
```bash
|
||||
streamlit run lib/ai_writers/ai_outline_writer/outline_ui.py
|
||||
```
|
||||
|
||||
2. Configure your outline:
|
||||
- Enter your blog topic
|
||||
- Select content type and depth
|
||||
- Choose outline style
|
||||
- Set target word count
|
||||
- Configure sections and subsections
|
||||
|
||||
3. Generate and edit:
|
||||
- Click "Generate Outline"
|
||||
- Review and edit sections
|
||||
- Customize content and images
|
||||
- Export in your preferred format
|
||||
|
||||
## 🔧 Configuration Options
|
||||
|
||||
### Basic Settings
|
||||
- **Blog Topic**: Main subject of your content
|
||||
- **Content Type**: Type of content to generate
|
||||
- **Content Depth**: Level of detail and complexity
|
||||
- **Outline Style**: Structure and formatting style
|
||||
|
||||
### Advanced Settings
|
||||
- **Target Word Count**: Desired length of the content
|
||||
- **Number of Sections**: Customize main sections
|
||||
- **Subsections**: Configure subsections per section
|
||||
- **Image Settings**: Customize image generation
|
||||
- **Target Audience**: Define your audience
|
||||
- **Language**: Select content language
|
||||
- **Keywords**: Add SEO keywords
|
||||
- **Excluded Topics**: Specify topics to avoid
|
||||
|
||||
## 📊 Output Formats
|
||||
|
||||
### 1. Preview Mode
|
||||
- Interactive preview of the entire outline
|
||||
- Real-time editing capabilities
|
||||
- Image preview and management
|
||||
- Content statistics
|
||||
|
||||
### 2. Markdown Export
|
||||
- Clean markdown formatting
|
||||
- Proper heading hierarchy
|
||||
- Image embedding
|
||||
- Code block formatting
|
||||
|
||||
### 3. JSON Export
|
||||
- Structured data format
|
||||
- Complete outline information
|
||||
- Content and image metadata
|
||||
- Configuration details
|
||||
|
||||
### 4. HTML Export
|
||||
- Styled HTML output
|
||||
- Responsive design
|
||||
- Image integration
|
||||
- Custom CSS support
|
||||
|
||||
## 💡 Best Practices
|
||||
|
||||
### Content Generation
|
||||
1. Start with a clear topic and target audience
|
||||
2. Choose appropriate content type and depth
|
||||
3. Use relevant keywords for SEO
|
||||
4. Review and edit generated content
|
||||
5. Add personal insights and examples
|
||||
|
||||
### Outline Structure
|
||||
1. Maintain logical flow between sections
|
||||
2. Balance section lengths
|
||||
3. Include relevant subsections
|
||||
4. Add appropriate transitions
|
||||
5. Ensure comprehensive coverage
|
||||
|
||||
### Image Usage
|
||||
1. Choose appropriate image styles
|
||||
2. Generate relevant images
|
||||
3. Optimize image placement
|
||||
4. Review image prompts
|
||||
5. Consider image licensing
|
||||
|
||||
## 🔄 Workflow
|
||||
|
||||
1. **Initial Setup**
|
||||
- Configure basic settings
|
||||
- Set content parameters
|
||||
- Define target audience
|
||||
|
||||
2. **Generation**
|
||||
- Generate initial outline
|
||||
- Review structure
|
||||
- Generate content
|
||||
- Create images
|
||||
|
||||
3. **Editing**
|
||||
- Review and edit content
|
||||
- Adjust structure
|
||||
- Customize images
|
||||
- Optimize for SEO
|
||||
|
||||
4. **Export**
|
||||
- Choose export format
|
||||
- Review final output
|
||||
- Download content
|
||||
- Save configuration
|
||||
|
||||
## 📝 Tips and Tricks
|
||||
|
||||
### Content Generation
|
||||
- Use specific keywords for better results
|
||||
- Provide clear context for the AI
|
||||
- Review and refine generated content
|
||||
- Add personal expertise
|
||||
|
||||
### Structure Optimization
|
||||
- Maintain consistent section lengths
|
||||
- Use clear subsection hierarchies
|
||||
- Include relevant examples
|
||||
- Add practical applications
|
||||
|
||||
### Image Enhancement
|
||||
- Use descriptive image prompts
|
||||
- Experiment with different styles
|
||||
- Consider image placement
|
||||
- Review image relevance
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please follow these steps:
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Submit a pull request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For support, please:
|
||||
1. Check the documentation
|
||||
2. Review existing issues
|
||||
3. Create a new issue if needed
|
||||
4. Contact the maintainers
|
||||
|
||||
## 🔮 Future Enhancements
|
||||
|
||||
Planned features:
|
||||
- Multi-language support
|
||||
- Advanced AI models
|
||||
- More export formats
|
||||
- Enhanced editing tools
|
||||
- Collaboration features
|
||||
- Version control integration
|
||||
- Analytics dashboard
|
||||
- Custom templates
|
||||
- API integration
|
||||
- Mobile optimization
|
||||
@@ -1,317 +0,0 @@
|
||||
"""
|
||||
Enhanced Blog Outline Generator
|
||||
|
||||
This module provides a sophisticated outline generation system that creates detailed,
|
||||
well-structured outlines for blog posts based on user preferences and content requirements.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import Dict, List, Optional
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from loguru import logger
|
||||
import json
|
||||
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
||||
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}")
|
||||
|
||||
class ContentType(Enum):
|
||||
"""Types of content that can be generated."""
|
||||
HOW_TO = "how-to"
|
||||
TUTORIAL = "tutorial"
|
||||
LISTICLE = "listicle"
|
||||
COMPARISON = "comparison"
|
||||
CASE_STUDY = "case-study"
|
||||
OPINION = "opinion"
|
||||
NEWS = "news"
|
||||
REVIEW = "review"
|
||||
GUIDE = "guide"
|
||||
|
||||
class ContentDepth(Enum):
|
||||
"""Depth levels for content coverage."""
|
||||
BASIC = "basic"
|
||||
INTERMEDIATE = "intermediate"
|
||||
ADVANCED = "advanced"
|
||||
EXPERT = "expert"
|
||||
|
||||
class OutlineStyle(Enum):
|
||||
"""Styles for outline structure."""
|
||||
TRADITIONAL = "traditional"
|
||||
MODERN = "modern"
|
||||
CONVERSATIONAL = "conversational"
|
||||
ACADEMIC = "academic"
|
||||
SEO_OPTIMIZED = "seo-optimized"
|
||||
|
||||
@dataclass
|
||||
class OutlineConfig:
|
||||
"""Configuration for outline generation."""
|
||||
content_type: ContentType = ContentType.GUIDE
|
||||
content_depth: ContentDepth = ContentDepth.INTERMEDIATE
|
||||
outline_style: OutlineStyle = OutlineStyle.MODERN
|
||||
target_word_count: int = 2000
|
||||
num_main_sections: int = 5
|
||||
num_subsections_per_section: int = 3
|
||||
include_introduction: bool = True
|
||||
include_conclusion: bool = True
|
||||
include_faqs: bool = True
|
||||
include_resources: bool = True
|
||||
target_audience: str = "general"
|
||||
language: str = "English"
|
||||
keywords: List[str] = None
|
||||
exclude_topics: List[str] = None
|
||||
include_images: bool = True
|
||||
image_style: str = "realistic"
|
||||
image_engine: str = "Gemini-AI"
|
||||
|
||||
@dataclass
|
||||
class SectionContent:
|
||||
"""Content for a section including text and image."""
|
||||
title: str
|
||||
content: str
|
||||
image_prompt: Optional[str] = None
|
||||
image_path: Optional[str] = None
|
||||
|
||||
class BlogOutlineGenerator:
|
||||
"""Enhanced blog outline generator with comprehensive controls."""
|
||||
|
||||
def __init__(self, config: Optional[OutlineConfig] = None):
|
||||
"""Initialize the outline generator with optional configuration."""
|
||||
self.config = config or OutlineConfig()
|
||||
self.outline = {}
|
||||
self.section_contents = {}
|
||||
|
||||
def generate_outline(self, topic: str) -> Dict[str, List[str]]:
|
||||
"""Generate a blog outline based on the topic and configuration."""
|
||||
try:
|
||||
# Create a focused prompt for outline generation
|
||||
prompt = f"""Generate a blog outline for topic: {topic}
|
||||
|
||||
Content Type: {self.config.content_type.value}
|
||||
Target Audience: {self.config.target_audience}
|
||||
Content Depth: {self.config.content_depth.value}
|
||||
Style: {self.config.outline_style.value}
|
||||
Word Count Target: {self.config.target_word_count}
|
||||
Main Sections: {self.config.num_main_sections}
|
||||
Subsections per Section: {self.config.num_subsections_per_section}
|
||||
|
||||
Requirements:
|
||||
- Create exactly {self.config.num_main_sections} main sections
|
||||
- Each section should have exactly {self.config.num_subsections_per_section} subsections
|
||||
- Focus on {self.config.content_type.value} content style
|
||||
- Target {self.config.target_audience} audience
|
||||
- Maintain {self.config.content_depth.value} depth
|
||||
- Follow {self.config.outline_style.value} style
|
||||
- Optimize for {self.config.target_word_count} words total
|
||||
|
||||
IMPORTANT: You must return a valid JSON object with main sections as keys and lists of subsections as values.
|
||||
Example format: {{"Section 1": ["Subsection 1.1", "Subsection 1.2"], "Section 2": ["Subsection 2.1", "Subsection 2.2"]}}
|
||||
Do not include any additional text or explanations, only the JSON object."""
|
||||
|
||||
# Get outline from LLM
|
||||
outline_json = llm_text_gen(prompt)
|
||||
|
||||
# Clean the response to ensure it's valid JSON
|
||||
outline_json = outline_json.strip()
|
||||
if not outline_json.startswith('{'):
|
||||
outline_json = outline_json[outline_json.find('{'):]
|
||||
if not outline_json.endswith('}'):
|
||||
outline_json = outline_json[:outline_json.rfind('}')+1]
|
||||
|
||||
# Parse the outline
|
||||
try:
|
||||
outline = json.loads(outline_json)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON parsing error: {str(e)}")
|
||||
logger.error(f"Raw response: {outline_json}")
|
||||
# Fallback to a basic outline structure
|
||||
outline = {
|
||||
f"Section {i+1}": [f"Subsection {i+1}.{j+1}" for j in range(self.config.num_subsections_per_section)]
|
||||
for i in range(self.config.num_main_sections)
|
||||
}
|
||||
|
||||
# Add introduction and conclusion if configured
|
||||
if self.config.include_introduction:
|
||||
outline = {"Introduction": ["Overview", "Importance", "What to Expect"]} | outline
|
||||
|
||||
if self.config.include_conclusion:
|
||||
outline["Conclusion"] = ["Summary", "Key Takeaways", "Next Steps"]
|
||||
|
||||
# Add FAQs if configured
|
||||
if self.config.include_faqs:
|
||||
# Generate topic-specific FAQs
|
||||
faq_prompt = f"""Generate 3 specific and relevant FAQ questions for a blog post about: {topic}
|
||||
|
||||
Content Type: {self.config.content_type.value}
|
||||
Target Audience: {self.config.target_audience}
|
||||
Content Depth: {self.config.content_depth.value}
|
||||
|
||||
Requirements:
|
||||
- Questions should be specific to the topic
|
||||
- Cover common concerns and important aspects
|
||||
- Be relevant to the target audience
|
||||
- Include both basic and advanced questions
|
||||
|
||||
Format: Return only a JSON array of 3 questions.
|
||||
Example format: ["Question 1?", "Question 2?", "Question 3?"]"""
|
||||
|
||||
try:
|
||||
faq_json = llm_text_gen(faq_prompt)
|
||||
faq_json = faq_json.strip()
|
||||
if not faq_json.startswith('['):
|
||||
faq_json = faq_json[faq_json.find('['):]
|
||||
if not faq_json.endswith(']'):
|
||||
faq_json = faq_json[:faq_json.rfind(']')+1]
|
||||
|
||||
faqs = json.loads(faq_json)
|
||||
outline["Frequently Asked Questions"] = faqs
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating FAQs: {str(e)}")
|
||||
outline["Frequently Asked Questions"] = [
|
||||
f"Common Question about {topic} 1",
|
||||
f"Common Question about {topic} 2",
|
||||
f"Common Question about {topic} 3"
|
||||
]
|
||||
|
||||
# Add resources if configured
|
||||
if self.config.include_resources:
|
||||
outline["Additional Resources"] = [
|
||||
"Further Reading",
|
||||
"Tools and References",
|
||||
"Related Topics"
|
||||
]
|
||||
|
||||
return outline
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating outline: {str(e)}")
|
||||
return {}
|
||||
|
||||
def generate_section_content(self, section: str, subsections: List[str]) -> Optional[SectionContent]:
|
||||
"""Generate content for a section."""
|
||||
try:
|
||||
# Create a focused prompt for content generation
|
||||
prompt = f"""Generate content for section: {section}
|
||||
|
||||
Subsections: {', '.join(subsections)}
|
||||
Content Type: {self.config.content_type.value}
|
||||
Target Audience: {self.config.target_audience}
|
||||
Content Depth: {self.config.content_depth.value}
|
||||
Style: {self.config.outline_style.value}
|
||||
Word Count Target: {self.config.target_word_count // self.config.num_main_sections}
|
||||
|
||||
Requirements:
|
||||
- Write content for each subsection
|
||||
- Maintain {self.config.content_depth.value} depth
|
||||
- Target {self.config.target_audience} audience
|
||||
- Follow {self.config.outline_style.value} style
|
||||
- Optimize for {self.config.target_word_count // self.config.num_main_sections} words
|
||||
- Include relevant examples and data points
|
||||
- Use clear, engaging language
|
||||
|
||||
Format: Return only a JSON object with 'content' and 'image_prompt' fields.
|
||||
Example format: {{"content": "Section content here...", "image_prompt": "Image description here..."}}"""
|
||||
|
||||
# Get content from LLM
|
||||
content_json = llm_text_gen(prompt)
|
||||
content_data = json.loads(content_json)
|
||||
|
||||
# Generate image if configured
|
||||
image_path = None
|
||||
if self.config.include_images:
|
||||
image_path = self.generate_section_image(section)
|
||||
|
||||
return SectionContent(
|
||||
title=section,
|
||||
content=content_data["content"],
|
||||
image_prompt=content_data.get("image_prompt"),
|
||||
image_path=image_path
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating content for section {section}: {str(e)}")
|
||||
return None
|
||||
|
||||
def generate_section_image(self, section: str) -> Optional[str]:
|
||||
"""Generate an image for a section."""
|
||||
try:
|
||||
# Create a focused prompt for image generation
|
||||
prompt = f"""Generate an image prompt for section: {section}
|
||||
|
||||
Style: {self.config.image_style}
|
||||
Engine: {self.config.image_engine}
|
||||
Content Type: {self.config.content_type.value}
|
||||
Target Audience: {self.config.target_audience}
|
||||
|
||||
Requirements:
|
||||
- Create a {self.config.image_style} style image
|
||||
- Optimize for {self.config.image_engine} engine
|
||||
- Match {self.config.content_type.value} content type
|
||||
- Appeal to {self.config.target_audience} audience
|
||||
- Be visually engaging and relevant
|
||||
|
||||
Format: Return only a JSON object with an 'image_prompt' field.
|
||||
Example format: {{"image_prompt": "Detailed image description here..."}}"""
|
||||
|
||||
# Get image prompt from LLM
|
||||
prompt_json = llm_text_gen(prompt)
|
||||
prompt_data = json.loads(prompt_json)
|
||||
|
||||
# Generate image using the specified engine
|
||||
if self.config.image_engine == "Gemini-AI":
|
||||
image_path = generate_gemini_image(prompt_data["image_prompt"])
|
||||
elif self.config.image_engine == "Dalle3":
|
||||
image_path = generate_dalle_image(prompt_data["image_prompt"])
|
||||
else: # Stability-AI
|
||||
image_path = generate_stability_image(prompt_data["image_prompt"])
|
||||
|
||||
return image_path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating image for section {section}: {str(e)}")
|
||||
return None
|
||||
|
||||
def to_markdown(self) -> str:
|
||||
"""Convert outline to markdown format with content and images."""
|
||||
markdown = f"# {self.outline.get('Introduction', [''])[0]}\n\n"
|
||||
|
||||
for section, subsections in self.outline.items():
|
||||
if section not in ["Introduction", "Conclusion", "FAQs", "Additional Resources"]:
|
||||
markdown += f"## {section}\n\n"
|
||||
|
||||
# Add section content if available
|
||||
if section in self.section_contents:
|
||||
content = self.section_contents[section]
|
||||
markdown += f"{content.content}\n\n"
|
||||
|
||||
# Add image if available
|
||||
if content.image_path:
|
||||
markdown += f"\n\n"
|
||||
|
||||
# Add subsections
|
||||
for subsection in subsections:
|
||||
markdown += f"- {subsection}\n"
|
||||
markdown += "\n"
|
||||
|
||||
if "Conclusion" in self.outline:
|
||||
markdown += "## Conclusion\n\n"
|
||||
for subsection in self.outline["Conclusion"]:
|
||||
markdown += f"- {subsection}\n"
|
||||
markdown += "\n"
|
||||
|
||||
if "FAQs" in self.outline:
|
||||
markdown += "## Frequently Asked Questions\n\n"
|
||||
for faq in self.outline["FAQs"]:
|
||||
markdown += f"- {faq}\n"
|
||||
markdown += "\n"
|
||||
|
||||
if "Additional Resources" in self.outline:
|
||||
markdown += "## Additional Resources\n\n"
|
||||
for resource in self.outline["Additional Resources"]:
|
||||
markdown += f"- {resource}\n"
|
||||
|
||||
return markdown
|
||||
@@ -1,739 +0,0 @@
|
||||
"""
|
||||
Streamlit UI for Enhanced Blog Outline Generator
|
||||
|
||||
This module provides a user-friendly interface for generating comprehensive blog outlines
|
||||
with AI-powered content and image generation capabilities.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from .get_blog_outline import (
|
||||
BlogOutlineGenerator,
|
||||
OutlineConfig,
|
||||
ContentType,
|
||||
ContentDepth,
|
||||
OutlineStyle
|
||||
)
|
||||
|
||||
# Custom CSS for better styling
|
||||
st.markdown("""
|
||||
<style>
|
||||
.main {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.stButton>button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 10px 24px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
}
|
||||
.stButton>button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
/* Add specific styling for the generate outline button */
|
||||
.generate-outline-button {
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.generate-outline-button > button {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.section-card {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
.content-preview {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
.stats-card {
|
||||
background-color: #e8f5e9;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
.edit-section {
|
||||
background-color: #e3f2fd;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
.subsection-list {
|
||||
margin-left: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
/* Main container width */
|
||||
.main .block-container {
|
||||
max-width: 100%;
|
||||
padding: 2rem;
|
||||
}
|
||||
/* Full width for the outline display */
|
||||
.outline-container {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
/* Section styling */
|
||||
.section-header {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
.subsection-item {
|
||||
font-size: 1.1rem;
|
||||
color: #34495e;
|
||||
margin: 0.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
/* Content area styling */
|
||||
.content-area {
|
||||
background-color: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin: 1rem 0;
|
||||
}
|
||||
/* Make sure all Streamlit elements use full width */
|
||||
.stMarkdown, .stText, .stTextArea, .stSelectbox, .stSlider {
|
||||
width: 100% !important;
|
||||
}
|
||||
/* Full width for code blocks */
|
||||
.stCodeBlock {
|
||||
width: 100% !important;
|
||||
}
|
||||
/* Full width for the main content */
|
||||
.main .block-container {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
/* Adjust the main content area */
|
||||
.main .block-container > div {
|
||||
max-width: 100%;
|
||||
}
|
||||
/* Make sure the outline content uses full width */
|
||||
.outline-content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
/* Adjust the preview section */
|
||||
.preview-section {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def edit_section_content(section: str, content: str) -> str:
|
||||
"""Edit section content with advanced options."""
|
||||
st.markdown('<div class="edit-section">', unsafe_allow_html=True)
|
||||
|
||||
# Content editing
|
||||
edited_content = st.text_area(
|
||||
"Edit Content",
|
||||
value=content,
|
||||
height=300,
|
||||
key=f"content_edit_{section}"
|
||||
)
|
||||
|
||||
# Word count and formatting
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
word_count = len(edited_content.split())
|
||||
st.info(f"Word Count: {word_count}")
|
||||
|
||||
with col2:
|
||||
formatting = st.multiselect(
|
||||
"Formatting Options",
|
||||
["Bold", "Italic", "Lists", "Code Blocks", "Links"],
|
||||
key=f"format_{section}"
|
||||
)
|
||||
|
||||
# AI enhancement options
|
||||
with st.expander("AI Enhancement Options"):
|
||||
enhance_options = st.multiselect(
|
||||
"Select Enhancements",
|
||||
["Improve Clarity", "Add Examples", "Expand Details", "Add Statistics", "Improve SEO"],
|
||||
key=f"enhance_{section}"
|
||||
)
|
||||
|
||||
if st.button("Apply Enhancements", key=f"apply_enhance_{section}"):
|
||||
with st.spinner("Applying enhancements..."):
|
||||
# TODO: Implement AI enhancement logic
|
||||
st.success("Enhancements applied!")
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
return edited_content
|
||||
|
||||
def edit_subsections(section: str, subsections: List[str]) -> List[str]:
|
||||
"""Edit subsections with reordering and editing capabilities."""
|
||||
st.markdown('<div class="edit-section">', unsafe_allow_html=True)
|
||||
|
||||
# Reorder subsections
|
||||
st.markdown("### Reorder Subsections")
|
||||
for i, subsection in enumerate(subsections):
|
||||
col1, col2 = st.columns([4, 1])
|
||||
with col1:
|
||||
subsections[i] = st.text_input(
|
||||
f"Subsection {i+1}",
|
||||
value=subsection,
|
||||
key=f"subsection_{section}_{i}"
|
||||
)
|
||||
with col2:
|
||||
if st.button("↑", key=f"move_up_{section}_{i}") and i > 0:
|
||||
subsections[i], subsections[i-1] = subsections[i-1], subsections[i]
|
||||
st.experimental_rerun()
|
||||
if st.button("↓", key=f"move_down_{section}_{i}") and i < len(subsections)-1:
|
||||
subsections[i], subsections[i+1] = subsections[i+1], subsections[i]
|
||||
st.experimental_rerun()
|
||||
|
||||
# Add/remove subsections
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
if st.button("Add Subsection", key=f"add_sub_{section}"):
|
||||
subsections.append("New Subsection")
|
||||
st.experimental_rerun()
|
||||
with col2:
|
||||
if st.button("Remove Last Subsection", key=f"remove_sub_{section}"):
|
||||
if subsections:
|
||||
subsections.pop()
|
||||
st.experimental_rerun()
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
return subsections
|
||||
|
||||
def edit_section_metadata(section: str, generator: BlogOutlineGenerator):
|
||||
"""Edit section metadata and settings."""
|
||||
st.markdown('<div class="edit-section">', unsafe_allow_html=True)
|
||||
|
||||
# Section settings
|
||||
st.markdown("### Section Settings")
|
||||
|
||||
# Image settings
|
||||
if generator.config.include_images:
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
new_image_style = st.selectbox(
|
||||
"Image Style",
|
||||
["realistic", "illustration", "minimalist", "photographic", "artistic"],
|
||||
key=f"img_style_{section}"
|
||||
)
|
||||
with col2:
|
||||
new_image_engine = st.selectbox(
|
||||
"Image Engine",
|
||||
["Gemini-AI", "Dalle3", "Stability-AI"],
|
||||
key=f"img_engine_{section}"
|
||||
)
|
||||
|
||||
if st.button("Regenerate Image", key=f"regen_img_{section}"):
|
||||
with st.spinner("Regenerating image..."):
|
||||
# TODO: Implement image regeneration logic
|
||||
st.success("Image regenerated!")
|
||||
|
||||
# Content settings
|
||||
st.markdown("### Content Settings")
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
target_word_count = st.number_input(
|
||||
"Target Word Count",
|
||||
min_value=100,
|
||||
max_value=2000,
|
||||
value=500,
|
||||
step=100,
|
||||
key=f"word_count_{section}"
|
||||
)
|
||||
with col2:
|
||||
content_depth = st.selectbox(
|
||||
"Content Depth",
|
||||
[depth.value for depth in ContentDepth],
|
||||
key=f"depth_{section}"
|
||||
)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
def display_section(section: str, subsections: List[str], content: Optional[Dict] = None, generator: Optional[BlogOutlineGenerator] = None):
|
||||
"""Display a section with its content and subsections."""
|
||||
st.markdown(f"""
|
||||
<div class="section-card">
|
||||
<div class="section-header">{section}</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Section editing controls
|
||||
col1, col2 = st.columns([4, 1])
|
||||
with col1:
|
||||
st.markdown(f"### {section}")
|
||||
with col2:
|
||||
edit_mode = st.checkbox("Edit Mode", key=f"edit_mode_{section}")
|
||||
|
||||
if content:
|
||||
# Display content with word count
|
||||
word_count = len(content.content.split())
|
||||
st.markdown(f"""
|
||||
<div class="content-preview">
|
||||
<p><strong>Content Preview</strong> ({word_count} words)</p>
|
||||
{content.content[:500]}...
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Image generation and display - Always show if images are enabled
|
||||
if generator and generator.config.include_images:
|
||||
st.markdown("### Image Generation")
|
||||
col1, col2, col3 = st.columns([2, 2, 1])
|
||||
|
||||
with col1:
|
||||
image_style = st.selectbox(
|
||||
"Image Style",
|
||||
["realistic", "illustration", "minimalist", "photographic", "artistic"],
|
||||
index=["realistic", "illustration", "minimalist", "photographic", "artistic"].index(generator.config.image_style),
|
||||
key=f"img_style_{section}"
|
||||
)
|
||||
|
||||
with col2:
|
||||
image_engine = st.selectbox(
|
||||
"Image Engine",
|
||||
["Gemini-AI", "Dalle3", "Stability-AI"],
|
||||
index=["Gemini-AI", "Dalle3", "Stability-AI"].index(generator.config.image_engine),
|
||||
key=f"img_engine_{section}"
|
||||
)
|
||||
|
||||
with col3:
|
||||
if st.button("Generate Image", key=f"gen_img_{section}"):
|
||||
with st.spinner(f"Generating image for {section}..."):
|
||||
# Update config with selected options
|
||||
generator.config.image_style = image_style
|
||||
generator.config.image_engine = image_engine
|
||||
image_path = generator.generate_section_image(section)
|
||||
if image_path:
|
||||
st.success("Image generated successfully!")
|
||||
st.experimental_rerun()
|
||||
else:
|
||||
st.error("Failed to generate image")
|
||||
|
||||
# Display existing image if available
|
||||
if content.image_path:
|
||||
st.markdown('<div class="image-container">', unsafe_allow_html=True)
|
||||
st.image(content.image_path, caption=section, use_column_width=True)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
# Display image prompt in expander
|
||||
if content.image_prompt:
|
||||
with st.expander("View Image Prompt"):
|
||||
st.code(content.image_prompt, language="text")
|
||||
|
||||
# Edit mode controls
|
||||
if edit_mode:
|
||||
st.markdown("### Edit Content")
|
||||
# Edit content
|
||||
edited_content = edit_section_content(section, content.content)
|
||||
if edited_content != content.content:
|
||||
content.content = edited_content
|
||||
st.experimental_rerun()
|
||||
|
||||
st.markdown("### Edit Subsections")
|
||||
# Edit subsections
|
||||
edited_subsections = edit_subsections(section, subsections)
|
||||
if edited_subsections != subsections:
|
||||
subsections[:] = edited_subsections
|
||||
st.experimental_rerun()
|
||||
|
||||
st.markdown("### Edit Metadata")
|
||||
# Edit metadata
|
||||
if generator:
|
||||
edit_section_metadata(section, generator)
|
||||
else:
|
||||
# Display subsections in view mode
|
||||
st.markdown("### Subsections")
|
||||
st.markdown('<div class="subsection-list">', unsafe_allow_html=True)
|
||||
for subsection in subsections:
|
||||
st.markdown(f'<div class="subsection-item">• {subsection}</div>', unsafe_allow_html=True)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown("</div>", unsafe_allow_html=True)
|
||||
|
||||
def display_stats(generator, outline):
|
||||
"""Display statistics about the generated outline."""
|
||||
total_sections = len(outline)
|
||||
total_subsections = sum(len(subsections) for subsections in outline.values())
|
||||
total_content = sum(len(content.content.split()) for content in generator.section_contents.values())
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.markdown(f"""
|
||||
<div class="stats-card">
|
||||
<h3>📊 Statistics</h3>
|
||||
<p>Total Sections: {total_sections}</p>
|
||||
<p>Total Subsections: {total_subsections}</p>
|
||||
<p>Estimated Word Count: {total_content}</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col2:
|
||||
st.markdown(f"""
|
||||
<div class="stats-card">
|
||||
<h3>🎯 Target</h3>
|
||||
<p>Target Word Count: {generator.config.target_word_count}</p>
|
||||
<p>Content Depth: {generator.config.content_depth.value}</p>
|
||||
<p>Style: {generator.config.outline_style.value}</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col3:
|
||||
st.markdown(f"""
|
||||
<div class="stats-card">
|
||||
<h3>📝 Content Type</h3>
|
||||
<p>Type: {generator.config.content_type.value}</p>
|
||||
<p>Audience: {generator.config.target_audience}</p>
|
||||
<p>Language: {generator.config.language}</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def main():
|
||||
# Header with description
|
||||
st.title("Blog Outline Generator")
|
||||
st.markdown("""
|
||||
Generate comprehensive blog outlines with AI-powered content and images.
|
||||
Customize your outline with various options and get detailed content for each section.
|
||||
""")
|
||||
|
||||
# Main content area with full width
|
||||
st.markdown('<div class="outline-container">', unsafe_allow_html=True)
|
||||
|
||||
# Move topic input to main area and make it more prominent
|
||||
st.markdown("### Enter Your Blog Topic")
|
||||
topic = st.text_input("", placeholder="Enter your blog topic here for creating outline...", key="blog_topic")
|
||||
|
||||
st.markdown("---") # Add a separator
|
||||
st.markdown("### Configuration Options")
|
||||
|
||||
# Create tabs for different configuration sections
|
||||
tab1, tab2, tab3, tab4 = st.tabs([
|
||||
"📝 Content Type & Target",
|
||||
"📊 Content Structure",
|
||||
"🎨 Style & Sections",
|
||||
"🖼️ Image & Optimization"
|
||||
])
|
||||
|
||||
with tab1:
|
||||
st.markdown("#### Content Type & Target")
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
content_type = st.selectbox(
|
||||
"Content Type",
|
||||
[type.value for type in ContentType],
|
||||
index=[type.value for type in ContentType].index(ContentType.GUIDE.value),
|
||||
help="Select the type of content you want to generate"
|
||||
)
|
||||
|
||||
with col2:
|
||||
target_audience = st.selectbox(
|
||||
"Target Audience",
|
||||
["General", "Technical", "Professional", "Academic", "Business", "Students", "Developers"],
|
||||
index=0,
|
||||
help="Select your target audience"
|
||||
)
|
||||
|
||||
with col3:
|
||||
language = st.selectbox(
|
||||
"Language",
|
||||
["English", "Spanish", "French", "German", "Italian", "Portuguese", "Chinese", "Japanese", "Korean"],
|
||||
index=0,
|
||||
help="Select the language for your content"
|
||||
)
|
||||
|
||||
with tab2:
|
||||
st.markdown("#### Content Structure")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
num_main_sections = st.slider(
|
||||
"Number of Main Sections",
|
||||
min_value=3,
|
||||
max_value=10,
|
||||
value=5,
|
||||
step=1,
|
||||
help="Choose how many main sections your outline should have"
|
||||
)
|
||||
|
||||
num_subsections = st.slider(
|
||||
"Subsections per Section",
|
||||
min_value=2,
|
||||
max_value=5,
|
||||
value=3,
|
||||
step=1,
|
||||
help="Choose how many subsections each main section should have"
|
||||
)
|
||||
|
||||
with col2:
|
||||
target_word_count = st.slider(
|
||||
"Target Word Count",
|
||||
min_value=500,
|
||||
max_value=5000,
|
||||
value=2000,
|
||||
step=100,
|
||||
help="Set your target word count for the entire blog post"
|
||||
)
|
||||
|
||||
# Display content statistics
|
||||
st.markdown("##### Content Statistics")
|
||||
st.markdown(f"""
|
||||
- Estimated Sections: {num_main_sections}
|
||||
- Total Subsections: {num_main_sections * num_subsections}
|
||||
- Target Word Count: {target_word_count}
|
||||
- Average Words per Section: {target_word_count // num_main_sections}
|
||||
""")
|
||||
|
||||
with tab3:
|
||||
st.markdown("#### Style & Sections")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
content_depth = st.selectbox(
|
||||
"Content Depth",
|
||||
[depth.value for depth in ContentDepth],
|
||||
index=[depth.value for depth in ContentDepth].index(ContentDepth.INTERMEDIATE.value),
|
||||
help="Select the depth of content coverage"
|
||||
)
|
||||
|
||||
outline_style = st.selectbox(
|
||||
"Outline Style",
|
||||
[style.value for style in OutlineStyle],
|
||||
index=[style.value for style in OutlineStyle].index(OutlineStyle.MODERN.value),
|
||||
help="Select the style of your outline"
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.markdown("##### Additional Sections")
|
||||
include_intro = st.checkbox("Include Introduction", value=True, help="Add an introduction section")
|
||||
include_conclusion = st.checkbox("Include Conclusion", value=True, help="Add a conclusion section")
|
||||
include_faqs = st.checkbox("Include FAQs", value=True, help="Add a FAQ section")
|
||||
include_resources = st.checkbox("Include Resources", value=True, help="Add a resources section")
|
||||
|
||||
with tab4:
|
||||
st.markdown("#### Image & Optimization")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("##### Image Settings")
|
||||
include_images = st.checkbox("Enable Image Generation", value=True, help="Enable AI image generation for sections")
|
||||
|
||||
if include_images:
|
||||
image_style = st.selectbox(
|
||||
"Image Style",
|
||||
["realistic", "illustration", "minimalist", "photographic", "artistic"],
|
||||
index=0,
|
||||
help="Select the style for generated images"
|
||||
)
|
||||
|
||||
image_engine = st.selectbox(
|
||||
"Image Engine",
|
||||
["Gemini-AI", "Dalle3", "Stability-AI"],
|
||||
index=0,
|
||||
help="Select the AI engine for image generation"
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.markdown("##### Content Optimization")
|
||||
keywords = st.text_area(
|
||||
"Keywords (comma-separated)",
|
||||
help="Enter keywords for SEO optimization, separated by commas"
|
||||
)
|
||||
|
||||
exclude_topics = st.text_area(
|
||||
"Topics to Exclude (comma-separated)",
|
||||
help="Enter topics you want to exclude from the content"
|
||||
)
|
||||
|
||||
st.markdown("---") # Add a separator before the generate button
|
||||
|
||||
# Create configuration
|
||||
config = OutlineConfig(
|
||||
content_type=ContentType(content_type),
|
||||
content_depth=ContentDepth(content_depth),
|
||||
outline_style=OutlineStyle(outline_style),
|
||||
target_word_count=target_word_count,
|
||||
num_main_sections=num_main_sections,
|
||||
num_subsections_per_section=num_subsections,
|
||||
include_introduction=include_intro,
|
||||
include_conclusion=include_conclusion,
|
||||
include_faqs=include_faqs,
|
||||
include_resources=include_resources,
|
||||
include_images=include_images,
|
||||
image_style=image_style if include_images else "realistic",
|
||||
image_engine=image_engine if include_images else "Gemini-AI",
|
||||
target_audience=target_audience,
|
||||
language=language,
|
||||
keywords=[k.strip() for k in keywords.split(',')] if keywords else None,
|
||||
exclude_topics=[t.strip() for t in exclude_topics.split(',')] if exclude_topics else None
|
||||
)
|
||||
|
||||
# Initialize generator
|
||||
generator = BlogOutlineGenerator(config)
|
||||
|
||||
# Store the generated outline in session state
|
||||
if 'outline' not in st.session_state:
|
||||
st.session_state.outline = None
|
||||
if 'section_contents' not in st.session_state:
|
||||
st.session_state.section_contents = {}
|
||||
|
||||
# Generate outline button with full width
|
||||
st.markdown('<div class="generate-outline-button">', unsafe_allow_html=True)
|
||||
if not topic:
|
||||
st.warning("Please enter a blog topic to generate the outline.")
|
||||
if st.button("Generate Outline", type="primary", use_container_width=True, disabled=not topic):
|
||||
with st.spinner("Generating outline and content..."):
|
||||
try:
|
||||
# Add progress bar
|
||||
progress_bar = st.progress(0)
|
||||
for i in range(100):
|
||||
time.sleep(0.01)
|
||||
progress_bar.progress(i + 1)
|
||||
|
||||
outline = generator.generate_outline(topic)
|
||||
st.session_state.outline = outline
|
||||
st.session_state.section_contents = generator.section_contents
|
||||
|
||||
# Display results
|
||||
st.success("Outline generated successfully!")
|
||||
|
||||
# Add copy button and display outline in full width
|
||||
st.markdown('<div class="outline-content">', unsafe_allow_html=True)
|
||||
outline_text = json.dumps(outline, indent=2)
|
||||
st.code(outline_text, language="json")
|
||||
st.button("Copy Outline", key="copy_outline",
|
||||
help="Copy the outline to clipboard",
|
||||
on_click=lambda: st.write(f'<script>navigator.clipboard.writeText(`{outline_text}`)</script>',
|
||||
unsafe_allow_html=True))
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
# Display statistics
|
||||
display_stats(generator, outline)
|
||||
|
||||
# Output format selection
|
||||
output_format = st.radio(
|
||||
"Output Format",
|
||||
["Preview", "Markdown", "JSON", "HTML"]
|
||||
)
|
||||
|
||||
if output_format == "Preview":
|
||||
# Display outline with content and images
|
||||
st.markdown('<div class="preview-section">', unsafe_allow_html=True)
|
||||
for section, subsections in outline.items():
|
||||
content = generator.section_contents.get(section)
|
||||
display_section(section, subsections, content, generator)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
elif output_format == "Markdown":
|
||||
markdown_output = generator.to_markdown()
|
||||
st.markdown('<div class="outline-content">', unsafe_allow_html=True)
|
||||
st.code(markdown_output, language="markdown")
|
||||
st.download_button(
|
||||
"Download Markdown",
|
||||
markdown_output,
|
||||
file_name="blog_outline.md",
|
||||
mime="text/markdown"
|
||||
)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
elif output_format == "JSON":
|
||||
json_output = json.dumps({
|
||||
"outline": outline,
|
||||
"contents": {
|
||||
section: {
|
||||
"title": content.title,
|
||||
"content": content.content,
|
||||
"image_prompt": content.image_prompt,
|
||||
"image_path": content.image_path
|
||||
}
|
||||
for section, content in generator.section_contents.items()
|
||||
}
|
||||
}, indent=2)
|
||||
st.markdown('<div class="outline-content">', unsafe_allow_html=True)
|
||||
st.code(json_output, language="json")
|
||||
st.download_button(
|
||||
"Download JSON",
|
||||
json_output,
|
||||
file_name="blog_outline.json",
|
||||
mime="application/json"
|
||||
)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
elif output_format == "HTML":
|
||||
html_output = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{topic} - Blog Outline</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; max-width: 100%; margin: 0 auto; padding: 20px; }}
|
||||
.section {{ margin-bottom: 30px; }}
|
||||
.content {{ background: #f8f9fa; padding: 15px; border-radius: 4px; }}
|
||||
img {{ max-width: 100%; height: auto; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{topic}</h1>
|
||||
{generator.to_markdown().replace('#', '##')}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
st.markdown('<div class="outline-content">', unsafe_allow_html=True)
|
||||
st.code(html_output, language="html")
|
||||
st.download_button(
|
||||
"Download HTML",
|
||||
html_output,
|
||||
file_name="blog_outline.html",
|
||||
mime="text/html"
|
||||
)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error generating outline: {str(e)}")
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
# Display the outline if it exists in session state
|
||||
if st.session_state.outline:
|
||||
st.markdown('<div class="preview-section">', unsafe_allow_html=True)
|
||||
for section, subsections in st.session_state.outline.items():
|
||||
content = st.session_state.section_contents.get(section)
|
||||
display_section(section, subsections, content, generator)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True) # Close the outline container
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,163 +0,0 @@
|
||||
# AI Blog Rewriter & Updater
|
||||
|
||||
A powerful AI-powered tool for rewriting and updating existing blog content with improved quality, factual accuracy, and SEO optimization.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Content Import
|
||||
- **URL Import**: Automatically extract content from any blog URL
|
||||
- **Manual Input**: Paste content directly with title, meta description, and author information
|
||||
- **Smart Content Extraction**: Preserves structure, headings, images, and metadata
|
||||
|
||||
### 2. Content Analysis
|
||||
- **Metrics Analysis**:
|
||||
- Word count
|
||||
- Sentence count
|
||||
- Paragraph count
|
||||
- Average words per sentence
|
||||
- Average sentences per paragraph
|
||||
- **Structure Analysis**:
|
||||
- Heading hierarchy
|
||||
- Content organization
|
||||
- Image analysis
|
||||
- **Age Analysis**:
|
||||
- Content age calculation
|
||||
- Publication date detection
|
||||
|
||||
### 3. Web Research
|
||||
- **Topic Extraction**: Automatically identifies key topics for fact-checking
|
||||
- **Multi-Source Research**: Gathers information from various sources
|
||||
- **Research Depth Control**: Choose between low, medium, and high research depth
|
||||
- **Source Organization**: Categorizes research by topic with source details
|
||||
|
||||
### 4. Rewriting Modes
|
||||
- **Standard Rewrite**: Improve clarity and flow while maintaining core message
|
||||
- **SEO Optimization**: Enhance content for search engines with targeted keywords
|
||||
- **Simplification**: Make complex content more accessible
|
||||
- **Expansion**: Add more details and examples
|
||||
- **Fact Check**: Update outdated information
|
||||
- **Tone Shift**: Change writing style while preserving content
|
||||
- **Modernization**: Update with current information and trends
|
||||
|
||||
### 5. Customization Options
|
||||
- **Tone Selection**:
|
||||
- Professional
|
||||
- Conversational
|
||||
- Academic
|
||||
- Enthusiastic
|
||||
- Authoritative
|
||||
- Friendly
|
||||
- Technical
|
||||
- Inspirational
|
||||
- **Length Control**:
|
||||
- Maintain original length
|
||||
- Create shorter version
|
||||
- Create longer version
|
||||
- Custom word count
|
||||
- **SEO Features**:
|
||||
- Focus keyword optimization
|
||||
- Meta description generation
|
||||
- Title optimization
|
||||
- **Special Instructions**: Add custom requirements for the rewrite
|
||||
|
||||
### 6. Image Generation
|
||||
- **AI Image Suggestions**: Get recommendations for relevant images
|
||||
- **Custom Image Generation**: Create images based on content
|
||||
- **Style Options**:
|
||||
- Realistic
|
||||
- Artistic
|
||||
- Cartoon
|
||||
- 3D Render
|
||||
- **Image Placement**: Suggested optimal placement within content
|
||||
|
||||
### 7. Export Options
|
||||
- **Preview Mode**: View formatted content
|
||||
- **Markdown Export**: Get clean markdown version
|
||||
- **Image Integration**: Include generated images with captions
|
||||
- **Meta Information**: Export with optimized title and meta description
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Import Content**
|
||||
- Choose between URL import or manual content entry
|
||||
- Provide necessary metadata (title, author, etc.)
|
||||
|
||||
2. **Analysis & Research**
|
||||
- Review content analysis metrics
|
||||
- Examine research findings
|
||||
- Identify areas for improvement
|
||||
|
||||
3. **Configure Rewrite Settings**
|
||||
- Select rewrite mode
|
||||
- Choose target tone
|
||||
- Set content length
|
||||
- Add focus keywords
|
||||
- Provide special instructions
|
||||
|
||||
4. **Review & Export**
|
||||
- Preview rewritten content
|
||||
- Generate suggested images
|
||||
- Export in desired format
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Dependencies
|
||||
- Streamlit for UI
|
||||
- BeautifulSoup for content extraction
|
||||
- GPT providers for text generation
|
||||
- Image generation capabilities
|
||||
- Web research APIs (Exa, Tavily)
|
||||
|
||||
### Key Components
|
||||
- `BlogRewriter` class: Core functionality
|
||||
- Content extraction and analysis
|
||||
- Research integration
|
||||
- AI-powered rewriting
|
||||
- Image generation
|
||||
- Export capabilities
|
||||
|
||||
### Error Handling
|
||||
- Robust error handling for URL extraction
|
||||
- Fallback mechanisms for content parsing
|
||||
- Graceful degradation for API failures
|
||||
- User-friendly error messages
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Content Import**
|
||||
- Use clean, well-structured URLs
|
||||
- Provide complete metadata for manual entry
|
||||
- Ensure content is properly formatted
|
||||
|
||||
2. **Research Settings**
|
||||
- Choose appropriate research depth
|
||||
- Review research findings carefully
|
||||
- Verify source credibility
|
||||
|
||||
3. **Rewrite Configuration**
|
||||
- Select appropriate tone for audience
|
||||
- Use relevant focus keywords
|
||||
- Provide clear special instructions
|
||||
|
||||
4. **Image Generation**
|
||||
- Use descriptive prompts
|
||||
- Choose appropriate style
|
||||
- Consider image placement
|
||||
|
||||
## Limitations
|
||||
|
||||
- Maximum content length for processing
|
||||
- API rate limits for research
|
||||
- Image generation constraints
|
||||
- Language support limitations
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Multi-language support
|
||||
- Advanced SEO analysis
|
||||
- Content structure templates
|
||||
- Collaborative editing
|
||||
- Integration with CMS platforms
|
||||
- Custom AI model selection
|
||||
- Advanced image editing
|
||||
- Content versioning
|
||||
@@ -1,11 +0,0 @@
|
||||
"""
|
||||
AI Blog Rewriter Module
|
||||
|
||||
This module provides the main entry point for the blog rewriter functionality,
|
||||
importing and using the utility and UI modules.
|
||||
"""
|
||||
|
||||
from .blog_rewriter_ui import write_blog_rewriter
|
||||
|
||||
if __name__ == "__main__":
|
||||
write_blog_rewriter()
|
||||
@@ -1,624 +0,0 @@
|
||||
"""
|
||||
Blog Rewriter UI Module
|
||||
|
||||
This module contains the Streamlit interface for the blog rewriter,
|
||||
providing a user-friendly way to interact with the rewriting functionality.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import json
|
||||
from datetime import datetime
|
||||
from .blog_rewriter_utils import BlogRewriter, REWRITE_MODES, TONE_OPTIONS, MAX_META_DESCRIPTION_LENGTH
|
||||
|
||||
def write_blog_rewriter():
|
||||
"""Main function to display the blog rewriter UI."""
|
||||
st.title("AI Blog Rewriter & Updater")
|
||||
|
||||
# Create a container for the header section
|
||||
with st.container():
|
||||
st.markdown("""
|
||||
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 10px; margin-bottom: 20px;">
|
||||
<h3 style="margin-top: 0;">Revitalize Your Content</h3>
|
||||
<p>Update, fact-check, and enhance your existing blog posts with AI assistance.
|
||||
Our tool analyzes your content, researches the latest information, and rewrites your blog
|
||||
to be more engaging, accurate, and SEO-friendly.</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Initialize the BlogRewriter class
|
||||
if "blog_rewriter" not in st.session_state:
|
||||
st.session_state.blog_rewriter = BlogRewriter()
|
||||
|
||||
# Initialize session state variables
|
||||
if "original_content" not in st.session_state:
|
||||
st.session_state.original_content = {}
|
||||
if "content_analysis" not in st.session_state:
|
||||
st.session_state.content_analysis = {}
|
||||
if "research_results" not in st.session_state:
|
||||
st.session_state.research_results = {}
|
||||
if "rewritten_content" not in st.session_state:
|
||||
st.session_state.rewritten_content = {}
|
||||
if "generated_images" not in st.session_state:
|
||||
st.session_state.generated_images = {}
|
||||
if "current_step" not in st.session_state:
|
||||
st.session_state.current_step = 1
|
||||
|
||||
# Create tabs for the workflow
|
||||
tab1, tab2, tab3, tab4 = st.tabs([
|
||||
"1️⃣ Import Content",
|
||||
"2️⃣ Analyze & Research",
|
||||
"3️⃣ Rewrite Settings",
|
||||
"4️⃣ Results & Export"
|
||||
])
|
||||
|
||||
# Tab 1: Import Content
|
||||
with tab1:
|
||||
st.header("Import Your Blog Content")
|
||||
|
||||
import_method = st.radio(
|
||||
"Choose import method:",
|
||||
["Import from URL", "Paste content manually"],
|
||||
horizontal=True
|
||||
)
|
||||
|
||||
if import_method == "Import from URL":
|
||||
url = st.text_input(
|
||||
"Enter blog URL:",
|
||||
placeholder="https://example.com/blog-post",
|
||||
help="Enter the full URL of the blog post you want to rewrite"
|
||||
)
|
||||
|
||||
if st.button("Import Content", type="primary"):
|
||||
if not url:
|
||||
st.error("Please enter a valid URL")
|
||||
else:
|
||||
with st.spinner("Extracting content from URL..."):
|
||||
# Extract content from URL
|
||||
st.session_state.original_content = st.session_state.blog_rewriter.extract_content_from_url(url)
|
||||
|
||||
if "error" in st.session_state.original_content:
|
||||
st.error(f"Error extracting content: {st.session_state.original_content['error']}")
|
||||
else:
|
||||
st.success("Content extracted successfully!")
|
||||
st.session_state.current_step = 2
|
||||
st.rerun()
|
||||
else:
|
||||
col1, col2 = st.columns([3, 1])
|
||||
|
||||
with col1:
|
||||
title = st.text_input(
|
||||
"Blog Title:",
|
||||
placeholder="Enter the title of your blog post"
|
||||
)
|
||||
|
||||
with col2:
|
||||
author = st.text_input(
|
||||
"Author (optional):",
|
||||
placeholder="Author name"
|
||||
)
|
||||
|
||||
meta_description = st.text_area(
|
||||
"Meta Description (optional):",
|
||||
placeholder="Enter the meta description of your blog post",
|
||||
max_chars=MAX_META_DESCRIPTION_LENGTH,
|
||||
height=80
|
||||
)
|
||||
|
||||
content = st.text_area(
|
||||
"Blog Content:",
|
||||
placeholder="Paste your blog content here...",
|
||||
height=300
|
||||
)
|
||||
|
||||
if st.button("Import Content", type="primary"):
|
||||
if not title or not content:
|
||||
st.error("Please enter both title and content")
|
||||
else:
|
||||
# Store the manually entered content
|
||||
st.session_state.original_content = {
|
||||
"title": title,
|
||||
"meta_description": meta_description,
|
||||
"content": content,
|
||||
"author": author,
|
||||
"headings": [],
|
||||
"images": [],
|
||||
"publish_date": None,
|
||||
"url": None
|
||||
}
|
||||
|
||||
st.success("Content imported successfully!")
|
||||
st.session_state.current_step = 2
|
||||
st.rerun()
|
||||
|
||||
# Display the imported content if available
|
||||
if st.session_state.original_content and "title" in st.session_state.original_content:
|
||||
with st.expander("View Imported Content", expanded=False):
|
||||
st.subheader(st.session_state.original_content["title"])
|
||||
|
||||
if st.session_state.original_content.get("meta_description"):
|
||||
st.markdown(f"**Meta Description:** {st.session_state.original_content['meta_description']}")
|
||||
|
||||
if st.session_state.original_content.get("author"):
|
||||
st.markdown(f"**Author:** {st.session_state.original_content['author']}")
|
||||
|
||||
if st.session_state.original_content.get("publish_date"):
|
||||
st.markdown(f"**Published:** {st.session_state.original_content['publish_date']}")
|
||||
|
||||
st.markdown("**Content Preview:**")
|
||||
content_preview = st.session_state.original_content["content"]
|
||||
if len(content_preview) > 1000:
|
||||
content_preview = content_preview[:1000] + "..."
|
||||
st.text_area("", content_preview, height=200, disabled=True)
|
||||
|
||||
# Display images if available
|
||||
if st.session_state.original_content.get("images"):
|
||||
st.markdown(f"**Images:** {len(st.session_state.original_content['images'])} images found")
|
||||
|
||||
# Tab 2: Analyze & Research
|
||||
with tab2:
|
||||
st.header("Analyze & Research")
|
||||
|
||||
if not st.session_state.original_content or "title" not in st.session_state.original_content:
|
||||
st.info("Please import your blog content first")
|
||||
else:
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
if st.button("Analyze Content", type="primary"):
|
||||
with st.spinner("Analyzing content..."):
|
||||
# Analyze the content
|
||||
st.session_state.content_analysis = st.session_state.blog_rewriter.analyze_content(
|
||||
st.session_state.original_content
|
||||
)
|
||||
st.success("Content analysis complete!")
|
||||
|
||||
with col2:
|
||||
research_depth = st.selectbox(
|
||||
"Research Depth:",
|
||||
["low", "medium", "high"],
|
||||
index=1,
|
||||
format_func=lambda x: {"low": "Basic", "medium": "Standard", "high": "Comprehensive"}[x],
|
||||
help="Choose the depth of research to update your content"
|
||||
)
|
||||
|
||||
if st.button("Conduct Research", type="primary"):
|
||||
with st.spinner("Researching latest information..."):
|
||||
# Conduct research
|
||||
st.session_state.research_results = st.session_state.blog_rewriter.conduct_research(
|
||||
st.session_state.original_content["title"],
|
||||
st.session_state.original_content["content"],
|
||||
research_depth
|
||||
)
|
||||
st.success("Research complete!")
|
||||
|
||||
# Display content analysis if available
|
||||
if st.session_state.content_analysis:
|
||||
st.subheader("Content Analysis")
|
||||
|
||||
metrics = st.session_state.content_analysis.get("metrics", {})
|
||||
|
||||
# Create metrics display
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
with col1:
|
||||
st.metric("Word Count", metrics.get("word_count", 0))
|
||||
with col2:
|
||||
st.metric("Paragraphs", metrics.get("paragraph_count", 0))
|
||||
with col3:
|
||||
st.metric("Sentences", metrics.get("sentence_count", 0))
|
||||
with col4:
|
||||
content_age = st.session_state.content_analysis.get("content_age", {})
|
||||
if "months" in content_age:
|
||||
st.metric("Content Age", f"{content_age['months']} months")
|
||||
elif "error" in content_age:
|
||||
st.metric("Content Age", "Unknown")
|
||||
|
||||
# Heading structure
|
||||
heading_structure = st.session_state.content_analysis.get("heading_structure", {})
|
||||
if heading_structure:
|
||||
st.markdown("**Heading Structure:**")
|
||||
for level, count in sorted(heading_structure.items()):
|
||||
st.markdown(f"H{level}: {count} headings")
|
||||
|
||||
# Image analysis
|
||||
images = st.session_state.content_analysis.get("images", {})
|
||||
if images:
|
||||
st.markdown(f"**Images:** {images.get('count', 0)} images found, {images.get('with_alt_text', 0)} with alt text")
|
||||
|
||||
# Display research results if available
|
||||
if st.session_state.research_results:
|
||||
st.subheader("Research Results")
|
||||
|
||||
topics = st.session_state.research_results.get("topics", [])
|
||||
if topics:
|
||||
for topic in topics:
|
||||
with st.expander(f"Topic: {topic['topic']}", expanded=False):
|
||||
for i, source in enumerate(topic.get("sources", [])):
|
||||
st.markdown(f"**Source {i+1}:** {source.get('title', 'Untitled')}")
|
||||
st.markdown(f"**URL:** {source.get('url', 'No URL')}")
|
||||
st.markdown(f"**Content Preview:** {source.get('content', 'No content')[:200]}...")
|
||||
st.markdown("---")
|
||||
else:
|
||||
st.info("No research results available")
|
||||
|
||||
# Enable proceeding to the next step if both analysis and research are done
|
||||
if st.session_state.content_analysis and st.session_state.research_results:
|
||||
if st.button("Proceed to Rewrite Settings", type="primary"):
|
||||
st.session_state.current_step = 3
|
||||
st.rerun()
|
||||
|
||||
# Tab 3: Rewrite Settings
|
||||
with tab3:
|
||||
st.header("Rewrite Settings")
|
||||
|
||||
if not st.session_state.original_content or "title" not in st.session_state.original_content:
|
||||
st.info("Please import your blog content first")
|
||||
elif not st.session_state.content_analysis or not st.session_state.research_results:
|
||||
st.info("Please complete content analysis and research first")
|
||||
else:
|
||||
# Create a form for rewrite settings
|
||||
with st.form("rewrite_settings_form"):
|
||||
st.subheader("Content Transformation")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
rewrite_mode = st.selectbox(
|
||||
"Rewrite Mode:",
|
||||
list(REWRITE_MODES.keys()),
|
||||
format_func=lambda x: x.replace("_", " ").title(),
|
||||
help="Choose how you want to transform your content"
|
||||
)
|
||||
|
||||
st.info(REWRITE_MODES[rewrite_mode])
|
||||
|
||||
with col2:
|
||||
tone = st.selectbox(
|
||||
"Target Tone:",
|
||||
TONE_OPTIONS,
|
||||
index=0,
|
||||
help="Choose the tone for your rewritten content"
|
||||
)
|
||||
|
||||
st.subheader("Content Length")
|
||||
|
||||
original_word_count = st.session_state.content_analysis.get("metrics", {}).get("word_count", 0)
|
||||
|
||||
length_option = st.radio(
|
||||
"Target Length:",
|
||||
["same", "shorter", "longer", "custom"],
|
||||
format_func=lambda x: {
|
||||
"same": f"Same as original ({original_word_count} words)",
|
||||
"shorter": f"Shorter (about {int(original_word_count * 0.7)} words)",
|
||||
"longer": f"Longer (about {int(original_word_count * 1.3)} words)",
|
||||
"custom": "Custom word count"
|
||||
}[x],
|
||||
horizontal=True
|
||||
)
|
||||
|
||||
if length_option == "custom":
|
||||
target_word_count = st.number_input(
|
||||
"Custom Word Count:",
|
||||
min_value=100,
|
||||
max_value=10000,
|
||||
value=original_word_count,
|
||||
step=100
|
||||
)
|
||||
else:
|
||||
target_word_count = {
|
||||
"same": original_word_count,
|
||||
"shorter": int(original_word_count * 0.7),
|
||||
"longer": int(original_word_count * 1.3)
|
||||
}[length_option]
|
||||
|
||||
st.subheader("SEO Optimization")
|
||||
|
||||
keywords = st.text_input(
|
||||
"Focus Keywords (comma-separated):",
|
||||
placeholder="e.g., digital marketing, SEO, content strategy",
|
||||
help="Enter keywords to optimize your content for"
|
||||
)
|
||||
|
||||
st.subheader("Additional Instructions")
|
||||
|
||||
special_instructions = st.text_area(
|
||||
"Special Instructions (optional):",
|
||||
placeholder="Add any specific instructions for rewriting your content...",
|
||||
help="Provide any additional instructions for the AI"
|
||||
)
|
||||
|
||||
# Submit button
|
||||
submitted = st.form_submit_button("Rewrite Blog", type="primary")
|
||||
|
||||
if submitted:
|
||||
# Process the form data
|
||||
user_preferences = {
|
||||
"rewrite_mode": rewrite_mode,
|
||||
"tone": tone,
|
||||
"target_word_count": target_word_count,
|
||||
"keywords": [k.strip() for k in keywords.split(",")] if keywords else [],
|
||||
"special_instructions": special_instructions
|
||||
}
|
||||
|
||||
with st.spinner("Rewriting your blog..."):
|
||||
# Rewrite the blog
|
||||
st.session_state.rewritten_content = st.session_state.blog_rewriter.rewrite_blog(
|
||||
st.session_state.original_content,
|
||||
user_preferences,
|
||||
st.session_state.research_results,
|
||||
st.session_state.content_analysis
|
||||
)
|
||||
|
||||
if "error" in st.session_state.rewritten_content:
|
||||
st.error(f"Error rewriting blog: {st.session_state.rewritten_content['error']}")
|
||||
else:
|
||||
st.success("Blog rewritten successfully!")
|
||||
st.session_state.current_step = 4
|
||||
st.rerun()
|
||||
|
||||
# Tab 4: Results & Export
|
||||
with tab4:
|
||||
st.header("Results & Export")
|
||||
|
||||
if not st.session_state.rewritten_content or "title" not in st.session_state.rewritten_content:
|
||||
st.info("Please complete the rewriting process first")
|
||||
else:
|
||||
# Display the rewritten content
|
||||
st.subheader("Rewritten Blog")
|
||||
|
||||
# Title and meta description
|
||||
st.markdown(f"## {st.session_state.rewritten_content['title']}")
|
||||
|
||||
if st.session_state.rewritten_content.get("meta_description"):
|
||||
with st.expander("Meta Description", expanded=True):
|
||||
st.text_area(
|
||||
"",
|
||||
st.session_state.rewritten_content["meta_description"],
|
||||
height=80,
|
||||
disabled=True
|
||||
)
|
||||
|
||||
# Create tabs for different views
|
||||
content_tab1, content_tab2 = st.tabs(["Preview", "Markdown"])
|
||||
|
||||
with content_tab1:
|
||||
st.markdown(st.session_state.rewritten_content["content"])
|
||||
|
||||
with content_tab2:
|
||||
st.text_area(
|
||||
"",
|
||||
st.session_state.rewritten_content["content"],
|
||||
height=400
|
||||
)
|
||||
|
||||
# Image generation section
|
||||
st.subheader("Generate Images")
|
||||
|
||||
suggested_images = st.session_state.rewritten_content.get("suggested_images", [])
|
||||
if suggested_images:
|
||||
st.markdown("**Suggested Images:**")
|
||||
|
||||
for i, img in enumerate(suggested_images):
|
||||
with st.expander(f"Image {i+1}: {img.get('description', 'No description')}", expanded=False):
|
||||
st.markdown(f"**Description:** {img.get('description', 'No description')}")
|
||||
st.markdown(f"**Caption:** {img.get('caption', 'No caption')}")
|
||||
st.markdown(f"**Placement:** {img.get('placement', 'No placement specified')}")
|
||||
|
||||
# Generate image button
|
||||
col1, col2 = st.columns([3, 1])
|
||||
|
||||
with col1:
|
||||
image_prompt = st.text_area(
|
||||
"Image Prompt:",
|
||||
value=img.get('description', ''),
|
||||
key=f"image_prompt_{i}"
|
||||
)
|
||||
|
||||
with col2:
|
||||
style = st.selectbox(
|
||||
"Style:",
|
||||
["realistic", "artistic", "cartoon", "3d_render"],
|
||||
key=f"style_{i}"
|
||||
)
|
||||
|
||||
if st.button("Generate Image", key=f"gen_img_{i}"):
|
||||
with st.spinner("Generating image..."):
|
||||
image_path = st.session_state.blog_rewriter.generate_image(image_prompt, style)
|
||||
|
||||
if image_path:
|
||||
# Store the generated image
|
||||
if "generated_images" not in st.session_state:
|
||||
st.session_state.generated_images = {}
|
||||
|
||||
st.session_state.generated_images[f"image_{i}"] = {
|
||||
"path": image_path,
|
||||
"caption": img.get('caption', ''),
|
||||
"placement": img.get('placement', '')
|
||||
}
|
||||
|
||||
st.success("Image generated successfully!")
|
||||
st.rerun()
|
||||
|
||||
# Display the generated image if available
|
||||
if f"image_{i}" in st.session_state.generated_images:
|
||||
st.image(
|
||||
st.session_state.generated_images[f"image_{i}"]["path"],
|
||||
caption=st.session_state.generated_images[f"image_{i}"]["caption"],
|
||||
use_column_width=True
|
||||
)
|
||||
else:
|
||||
st.info("No image suggestions available")
|
||||
|
||||
# Custom image generation
|
||||
with st.expander("Generate Custom Image", expanded=True):
|
||||
col1, col2 = st.columns([3, 1])
|
||||
|
||||
with col1:
|
||||
custom_image_prompt = st.text_area(
|
||||
"Image Prompt:",
|
||||
placeholder="Describe the image you want to generate..."
|
||||
)
|
||||
|
||||
with col2:
|
||||
custom_style = st.selectbox(
|
||||
"Style:",
|
||||
["realistic", "artistic", "cartoon", "3d_render"]
|
||||
)
|
||||
|
||||
if st.button("Generate Custom Image"):
|
||||
if not custom_image_prompt:
|
||||
st.error("Please enter an image prompt")
|
||||
else:
|
||||
with st.spinner("Generating image..."):
|
||||
image_path = st.session_state.blog_rewriter.generate_image(custom_image_prompt, custom_style)
|
||||
|
||||
if image_path:
|
||||
# Store the generated image
|
||||
if "generated_images" not in st.session_state:
|
||||
st.session_state.generated_images = {}
|
||||
|
||||
st.session_state.generated_images["custom_image"] = {
|
||||
"path": image_path,
|
||||
"caption": "Custom generated image",
|
||||
"placement": "Custom placement"
|
||||
}
|
||||
|
||||
st.success("Image generated successfully!")
|
||||
st.rerun()
|
||||
|
||||
# Display the generated custom image if available
|
||||
if "custom_image" in st.session_state.generated_images:
|
||||
st.image(
|
||||
st.session_state.generated_images["custom_image"]["path"],
|
||||
caption=st.session_state.generated_images["custom_image"]["caption"],
|
||||
use_column_width=True
|
||||
)
|
||||
|
||||
# Export options
|
||||
st.subheader("Export Options")
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
st.download_button(
|
||||
"Download as Markdown",
|
||||
data=st.session_state.rewritten_content["content"],
|
||||
file_name=f"{st.session_state.rewritten_content['title'].replace(' ', '_')}.md",
|
||||
mime="text/markdown"
|
||||
)
|
||||
|
||||
with col2:
|
||||
# Create HTML version
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{st.session_state.rewritten_content['title']}</title>
|
||||
<meta name="description" content="{st.session_state.rewritten_content.get('meta_description', '')}">
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }}
|
||||
h1, h2, h3, h4, h5, h6 {{ color: #333; }}
|
||||
img {{ max-width: 100%; height: auto; }}
|
||||
pre {{ background-color: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto; }}
|
||||
blockquote {{ border-left: 5px solid #eee; padding-left: 15px; margin-left: 0; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{st.session_state.rewritten_content['title']}</h1>
|
||||
{st.session_state.rewritten_content['content']}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
st.download_button(
|
||||
"Download as HTML",
|
||||
data=html_content,
|
||||
file_name=f"{st.session_state.rewritten_content['title'].replace(' ', '_')}.html",
|
||||
mime="text/html"
|
||||
)
|
||||
|
||||
with col3:
|
||||
# Create JSON version with all content and metadata
|
||||
json_content = {
|
||||
"title": st.session_state.rewritten_content["title"],
|
||||
"meta_description": st.session_state.rewritten_content.get("meta_description", ""),
|
||||
"content": st.session_state.rewritten_content["content"],
|
||||
"suggested_images": st.session_state.rewritten_content.get("suggested_images", []),
|
||||
"generated_images": [
|
||||
{
|
||||
"caption": img_data["caption"],
|
||||
"placement": img_data["placement"],
|
||||
"path": img_data["path"]
|
||||
}
|
||||
for img_key, img_data in st.session_state.generated_images.items()
|
||||
] if hasattr(st.session_state, "generated_images") else [],
|
||||
"original_title": st.session_state.original_content.get("title", ""),
|
||||
"original_url": st.session_state.original_content.get("url", ""),
|
||||
"rewrite_date": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
st.download_button(
|
||||
"Download as JSON",
|
||||
data=json.dumps(json_content, indent=2),
|
||||
file_name=f"{st.session_state.rewritten_content['title'].replace(' ', '_')}.json",
|
||||
mime="application/json"
|
||||
)
|
||||
|
||||
# Copy to clipboard buttons
|
||||
st.subheader("Quick Copy")
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
if st.button("Copy Title", key="copy_title"):
|
||||
st.code(st.session_state.rewritten_content["title"])
|
||||
st.success("Title copied to clipboard!")
|
||||
|
||||
with col2:
|
||||
if st.button("Copy Meta Description", key="copy_meta"):
|
||||
st.code(st.session_state.rewritten_content.get("meta_description", ""))
|
||||
st.success("Meta description copied to clipboard!")
|
||||
|
||||
with col3:
|
||||
if st.button("Copy Full Content", key="copy_content"):
|
||||
st.success("Content copied to clipboard!")
|
||||
|
||||
# Comparison with original
|
||||
with st.expander("Compare with Original", expanded=False):
|
||||
comp_col1, comp_col2 = st.columns(2)
|
||||
|
||||
with comp_col1:
|
||||
st.subheader("Original")
|
||||
st.markdown(f"**Title:** {st.session_state.original_content.get('title', '')}")
|
||||
if st.session_state.original_content.get("meta_description"):
|
||||
st.markdown(f"**Meta Description:** {st.session_state.original_content['meta_description']}")
|
||||
st.text_area(
|
||||
"Original Content",
|
||||
st.session_state.original_content.get("content", ""),
|
||||
height=300,
|
||||
disabled=True
|
||||
)
|
||||
|
||||
with comp_col2:
|
||||
st.subheader("Rewritten")
|
||||
st.markdown(f"**Title:** {st.session_state.rewritten_content['title']}")
|
||||
if st.session_state.rewritten_content.get("meta_description"):
|
||||
st.markdown(f"**Meta Description:** {st.session_state.rewritten_content['meta_description']}")
|
||||
st.text_area(
|
||||
"Rewritten Content",
|
||||
st.session_state.rewritten_content["content"],
|
||||
height=300,
|
||||
disabled=True
|
||||
)
|
||||
|
||||
# Start over button
|
||||
if st.button("Start Over", type="primary"):
|
||||
# Reset session state
|
||||
for key in ["original_content", "content_analysis", "research_results",
|
||||
"rewritten_content", "generated_images", "current_step"]:
|
||||
if key in st.session_state:
|
||||
del st.session_state[key]
|
||||
|
||||
st.rerun()
|
||||
|
||||
if __name__ == "__main__":
|
||||
write_blog_rewriter()
|
||||
@@ -1,595 +0,0 @@
|
||||
"""
|
||||
Blog Rewriter Utilities Module
|
||||
|
||||
This module contains the core functionality for rewriting and updating blog content,
|
||||
including content extraction, analysis, research, and rewriting capabilities.
|
||||
"""
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# Import required modules from the project
|
||||
from ...gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from ...gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
||||
from ...ai_web_researcher.metaphor_basic_neural_web_search import metaphor_search_articles
|
||||
from ...ai_web_researcher.tavily_ai_search import do_tavily_ai_search
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Define constants
|
||||
MAX_TITLE_LENGTH = 70
|
||||
MAX_META_DESCRIPTION_LENGTH = 160
|
||||
REWRITE_MODES = {
|
||||
"standard": "Standard rewrite with improved clarity and flow",
|
||||
"seo_optimization": "Optimize for search engines with targeted keywords",
|
||||
"simplification": "Simplify complex content for broader audience",
|
||||
"expansion": "Expand with additional details and examples",
|
||||
"fact_check": "Focus on fact-checking and updating information",
|
||||
"tone_shift": "Change the tone while preserving content",
|
||||
"modernization": "Update outdated content with current information"
|
||||
}
|
||||
|
||||
# Define tone options
|
||||
TONE_OPTIONS = [
|
||||
"Professional", "Conversational", "Academic", "Enthusiastic",
|
||||
"Authoritative", "Friendly", "Technical", "Inspirational"
|
||||
]
|
||||
|
||||
class BlogRewriter:
|
||||
"""Class to handle blog rewriting functionality."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the BlogRewriter class."""
|
||||
self.original_content = {}
|
||||
self.rewritten_content = {}
|
||||
self.research_results = {}
|
||||
self.content_analysis = {}
|
||||
self.image_suggestions = []
|
||||
|
||||
def extract_content_from_url(self, url: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract content from a given URL.
|
||||
|
||||
Args:
|
||||
url: The URL to extract content from
|
||||
|
||||
Returns:
|
||||
Dictionary containing extracted content
|
||||
"""
|
||||
logger.info(f"Extracting content from URL: {url}")
|
||||
|
||||
try:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
response = requests.get(url, headers=headers, timeout=15)
|
||||
response.raise_for_status()
|
||||
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
# Extract title
|
||||
title = soup.title.string if soup.title else ""
|
||||
|
||||
# Extract meta description
|
||||
meta_desc = ""
|
||||
meta_tag = soup.find("meta", attrs={"name": "description"})
|
||||
if meta_tag and "content" in meta_tag.attrs:
|
||||
meta_desc = meta_tag["content"]
|
||||
|
||||
# Extract main content - try multiple strategies
|
||||
content = ""
|
||||
|
||||
# Strategy 1: Look for article tag
|
||||
article_tag = soup.find("article")
|
||||
if article_tag:
|
||||
content = article_tag.get_text(separator="\n\n")
|
||||
|
||||
# Strategy 2: Look for main content areas
|
||||
if not content:
|
||||
main_content = soup.find(["main", "div", "section"], class_=re.compile(r"content|article|post|entry|main|body"))
|
||||
if main_content:
|
||||
for elem in main_content.find_all(["nav", "aside", "footer", "comments", "script", "style", "header"]):
|
||||
elem.decompose()
|
||||
content = main_content.get_text(separator="\n\n")
|
||||
|
||||
# Strategy 3: Look for specific content classes
|
||||
if not content:
|
||||
content_classes = ["post-content", "entry-content", "article-content", "blog-content", "content-area"]
|
||||
for class_name in content_classes:
|
||||
content_div = soup.find("div", class_=class_name)
|
||||
if content_div:
|
||||
for elem in content_div.find_all(["nav", "aside", "footer", "comments", "script", "style", "header"]):
|
||||
elem.decompose()
|
||||
content = content_div.get_text(separator="\n\n")
|
||||
break
|
||||
|
||||
# Strategy 4: Look for content within body
|
||||
if not content:
|
||||
body = soup.find("body")
|
||||
if body:
|
||||
# Remove unwanted elements
|
||||
for elem in body.find_all(["nav", "aside", "footer", "comments", "script", "style", "header"]):
|
||||
elem.decompose()
|
||||
content = body.get_text(separator="\n\n")
|
||||
|
||||
# Clean up the content
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
content = re.sub(r'\s{2,}', ' ', content)
|
||||
content = content.strip()
|
||||
|
||||
# Extract headings with their hierarchy
|
||||
headings = []
|
||||
for h in soup.find_all(["h1", "h2", "h3", "h4", "h5", "h6"]):
|
||||
headings.append({
|
||||
"level": int(h.name[1]),
|
||||
"text": h.get_text().strip()
|
||||
})
|
||||
|
||||
# Extract images with more metadata
|
||||
images = []
|
||||
for img in soup.find_all("img"):
|
||||
if img.get("src") and not img.get("src").startswith("data:"):
|
||||
image_url = img.get("src")
|
||||
if not image_url.startswith(("http://", "https://")):
|
||||
base_url = "/".join(url.split("/")[:3])
|
||||
image_url = f"{base_url}/{image_url.lstrip('/')}"
|
||||
|
||||
images.append({
|
||||
"url": image_url,
|
||||
"alt_text": img.get("alt", ""),
|
||||
"title": img.get("title", ""),
|
||||
"class": img.get("class", []),
|
||||
"width": img.get("width"),
|
||||
"height": img.get("height")
|
||||
})
|
||||
|
||||
# Extract publish date with multiple strategies
|
||||
publish_date = None
|
||||
# Try meta tags first
|
||||
date_meta = soup.find("meta", attrs={"property": "article:published_time"})
|
||||
if date_meta and "content" in date_meta.attrs:
|
||||
publish_date = date_meta["content"]
|
||||
else:
|
||||
# Try other meta tags
|
||||
for prop in ["datePublished", "dateCreated", "dateModified"]:
|
||||
date_meta = soup.find("meta", attrs={"property": prop})
|
||||
if date_meta and "content" in date_meta.attrs:
|
||||
publish_date = date_meta["content"]
|
||||
break
|
||||
|
||||
# Try HTML elements if meta tags failed
|
||||
if not publish_date:
|
||||
date_elem = soup.find(["time", "span", "div"], class_=re.compile(r"date|time|publish|posted|created"))
|
||||
if date_elem and date_elem.get_text():
|
||||
publish_date = date_elem.get_text().strip()
|
||||
|
||||
# Extract author with multiple strategies
|
||||
author = None
|
||||
# Try meta tags first
|
||||
author_meta = soup.find("meta", attrs={"name": "author"})
|
||||
if author_meta and "content" in author_meta.attrs:
|
||||
author = author_meta["content"]
|
||||
else:
|
||||
# Try other meta tags
|
||||
for prop in ["article:author", "author"]:
|
||||
author_meta = soup.find("meta", attrs={"property": prop})
|
||||
if author_meta and "content" in author_meta.attrs:
|
||||
author = author_meta["content"]
|
||||
break
|
||||
|
||||
# Try HTML elements if meta tags failed
|
||||
if not author:
|
||||
author_elem = soup.find(["a", "span", "div"], class_=re.compile(r"author|byline|writer|posted-by"))
|
||||
if author_elem and author_elem.get_text():
|
||||
author = author_elem.get_text().strip()
|
||||
|
||||
# Log content extraction results
|
||||
logger.info(f"Extracted content length: {len(content)} characters")
|
||||
logger.info(f"Found {len(headings)} headings")
|
||||
logger.info(f"Found {len(images)} images")
|
||||
logger.info(f"Publish date: {publish_date}")
|
||||
logger.info(f"Author: {author}")
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"meta_description": meta_desc,
|
||||
"content": content,
|
||||
"headings": headings,
|
||||
"images": images,
|
||||
"publish_date": publish_date,
|
||||
"author": author,
|
||||
"url": url
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting content from URL: {e}")
|
||||
return {
|
||||
"title": "",
|
||||
"meta_description": "",
|
||||
"content": "",
|
||||
"headings": [],
|
||||
"images": [],
|
||||
"publish_date": None,
|
||||
"author": None,
|
||||
"url": url,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def analyze_content(self, content: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze the extracted content to provide insights.
|
||||
|
||||
Args:
|
||||
content: Dictionary containing extracted content
|
||||
|
||||
Returns:
|
||||
Dictionary containing content analysis
|
||||
"""
|
||||
logger.info("Analyzing content")
|
||||
|
||||
analysis = {}
|
||||
|
||||
# Basic metrics
|
||||
text_content = content.get("content", "")
|
||||
word_count = len(text_content.split())
|
||||
sentence_count = len(re.split(r'[.!?]+', text_content))
|
||||
paragraph_count = len(re.split(r'\n\n+', text_content))
|
||||
|
||||
analysis["metrics"] = {
|
||||
"word_count": word_count,
|
||||
"sentence_count": sentence_count,
|
||||
"paragraph_count": paragraph_count,
|
||||
"avg_words_per_sentence": round(word_count / max(sentence_count, 1), 1),
|
||||
"avg_sentences_per_paragraph": round(sentence_count / max(paragraph_count, 1), 1)
|
||||
}
|
||||
|
||||
# Heading structure analysis
|
||||
headings = content.get("headings", [])
|
||||
heading_structure = {}
|
||||
for h in headings:
|
||||
level = h["level"]
|
||||
if level not in heading_structure:
|
||||
heading_structure[level] = 0
|
||||
heading_structure[level] += 1
|
||||
|
||||
analysis["heading_structure"] = heading_structure
|
||||
|
||||
# Content age analysis
|
||||
publish_date = content.get("publish_date")
|
||||
if publish_date:
|
||||
try:
|
||||
if "T" in publish_date:
|
||||
pub_date = datetime.fromisoformat(publish_date.replace("Z", "+00:00"))
|
||||
else:
|
||||
date_formats = [
|
||||
"%Y-%m-%d", "%d-%m-%Y", "%B %d, %Y", "%b %d, %Y",
|
||||
"%d %B %Y", "%d %b %Y", "%Y/%m/%d", "%d/%m/%Y"
|
||||
]
|
||||
for fmt in date_formats:
|
||||
try:
|
||||
pub_date = datetime.strptime(publish_date, fmt)
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
now = datetime.now()
|
||||
age_days = (now - pub_date).days
|
||||
analysis["content_age"] = {
|
||||
"days": age_days,
|
||||
"months": round(age_days / 30, 1),
|
||||
"years": round(age_days / 365, 1)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not parse publish date: {e}")
|
||||
analysis["content_age"] = {"error": "Could not determine content age"}
|
||||
else:
|
||||
analysis["content_age"] = {"error": "No publish date found"}
|
||||
|
||||
# Image analysis
|
||||
images = content.get("images", [])
|
||||
analysis["images"] = {
|
||||
"count": len(images),
|
||||
"with_alt_text": sum(1 for img in images if img.get("alt_text"))
|
||||
}
|
||||
|
||||
return analysis
|
||||
|
||||
def conduct_research(self, title: str, content: str, research_depth: str = "medium") -> Dict[str, Any]:
|
||||
"""
|
||||
Conduct web research to find updated information related to the blog content.
|
||||
|
||||
Args:
|
||||
title: Blog title
|
||||
content: Blog content
|
||||
research_depth: Depth of research (low, medium, high)
|
||||
|
||||
Returns:
|
||||
Dictionary containing research results
|
||||
"""
|
||||
logger.info(f"Conducting research with depth: {research_depth}")
|
||||
|
||||
# Extract key topics from the content
|
||||
prompt = f"""
|
||||
Extract 3-5 key topics or claims from this blog content that might need fact-checking or updating.
|
||||
For each topic, provide a concise search query that would help find the most recent information.
|
||||
|
||||
Blog title: {title}
|
||||
|
||||
First 1000 characters of content:
|
||||
{content[:1000]}...
|
||||
|
||||
Format your response as a JSON array of objects with 'topic' and 'query' fields.
|
||||
"""
|
||||
|
||||
try:
|
||||
topics_json = llm_text_gen(prompt)
|
||||
topics_json = re.search(r'\[.*\]', topics_json, re.DOTALL)
|
||||
if topics_json:
|
||||
topics = json.loads(topics_json.group(0))
|
||||
else:
|
||||
topics = [
|
||||
{"topic": title, "query": title + " latest information"},
|
||||
{"topic": "Updates on " + title, "query": title + " recent developments"}
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting topics: {e}")
|
||||
topics = [
|
||||
{"topic": title, "query": title + " latest information"},
|
||||
{"topic": "Updates on " + title, "query": title + " recent developments"}
|
||||
]
|
||||
|
||||
# Determine number of results based on research depth
|
||||
num_results = {"low": 2, "medium": 3, "high": 5}.get(research_depth, 3)
|
||||
|
||||
research_results = {"topics": []}
|
||||
|
||||
# Conduct research for each topic
|
||||
for topic in topics[:3]: # Limit to 3 topics
|
||||
topic_results = {"topic": topic["topic"], "sources": []}
|
||||
|
||||
# Try Exa search first
|
||||
try:
|
||||
exa_results = metaphor_search_articles(topic["query"], num_results=num_results)
|
||||
if exa_results:
|
||||
topic_results["sources"].extend(exa_results)
|
||||
except Exception as e:
|
||||
logger.warning(f"Exa search failed: {e}")
|
||||
|
||||
# If Exa didn't return enough results, try Tavily
|
||||
if len(topic_results["sources"]) < num_results:
|
||||
try:
|
||||
tavily_results = do_tavily_ai_search(topic["query"], num_results=num_results)
|
||||
if tavily_results:
|
||||
existing_urls = [s["url"] for s in topic_results["sources"]]
|
||||
for result in tavily_results:
|
||||
if result["url"] not in existing_urls:
|
||||
topic_results["sources"].append(result)
|
||||
existing_urls.append(result["url"])
|
||||
except Exception as e:
|
||||
logger.warning(f"Tavily search failed: {e}")
|
||||
|
||||
research_results["topics"].append(topic_results)
|
||||
|
||||
return research_results
|
||||
|
||||
def generate_rewrite_prompt(self, original_content: Dict[str, Any],
|
||||
user_preferences: Dict[str, Any],
|
||||
research_results: Dict[str, Any],
|
||||
content_analysis: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generate a prompt for the LLM to rewrite the blog.
|
||||
|
||||
Args:
|
||||
original_content: Original blog content
|
||||
user_preferences: User preferences for rewriting
|
||||
research_results: Research results for updating content
|
||||
content_analysis: Analysis of the original content
|
||||
|
||||
Returns:
|
||||
Prompt string for the LLM
|
||||
"""
|
||||
logger.info("Generating rewrite prompt")
|
||||
|
||||
# Extract key information
|
||||
title = original_content.get("title", "")
|
||||
content = original_content.get("content", "")
|
||||
|
||||
# Truncate content if it's too long
|
||||
max_content_length = 6000
|
||||
if len(content) > max_content_length:
|
||||
content_preview = content[:max_content_length] + "...\n[Content truncated due to length]"
|
||||
else:
|
||||
content_preview = content
|
||||
|
||||
# Format research results
|
||||
research_summary = ""
|
||||
for topic in research_results.get("topics", []):
|
||||
research_summary += f"\n## {topic['topic']}\n"
|
||||
for i, source in enumerate(topic.get("sources", [])[:3]):
|
||||
research_summary += f"Source {i+1}: {source.get('title', 'Untitled')}\n"
|
||||
research_summary += f"URL: {source.get('url', 'No URL')}\n"
|
||||
research_summary += f"Content: {source.get('content', 'No content')[:300]}...\n\n"
|
||||
|
||||
# Build the prompt
|
||||
prompt = f"""
|
||||
# Blog Rewriting Task
|
||||
|
||||
## Original Blog Information
|
||||
Title: {title}
|
||||
Word Count: {content_analysis.get('metrics', {}).get('word_count', 'Unknown')}
|
||||
Estimated Age: {content_analysis.get('content_age', {}).get('months', 'Unknown')} months
|
||||
|
||||
## Rewriting Instructions
|
||||
Mode: {user_preferences.get('rewrite_mode', 'standard')}
|
||||
Target Tone: {user_preferences.get('tone', 'Professional')}
|
||||
Target Word Count: {user_preferences.get('target_word_count', 'Same as original')}
|
||||
Focus Keywords: {', '.join(user_preferences.get('keywords', []))}
|
||||
|
||||
## Special Instructions
|
||||
{user_preferences.get('special_instructions', 'No special instructions')}
|
||||
|
||||
## Recent Research Findings
|
||||
{research_summary if research_summary else "No research results available."}
|
||||
|
||||
## Original Content
|
||||
{content_preview}
|
||||
|
||||
## Your Task
|
||||
Please rewrite this blog post according to the instructions above. The rewritten blog should:
|
||||
|
||||
1. Maintain the core message and value of the original content
|
||||
2. Update any outdated information based on the research findings
|
||||
3. Adopt the requested tone and style
|
||||
4. Incorporate the focus keywords naturally
|
||||
5. Improve readability and engagement
|
||||
6. Maintain a logical structure with appropriate headings
|
||||
7. Include a compelling introduction and conclusion
|
||||
|
||||
## Output Format
|
||||
Please provide your response in the following JSON format:
|
||||
```json
|
||||
{{
|
||||
"title": "Rewritten title",
|
||||
"meta_description": "SEO-optimized meta description (max 160 characters)",
|
||||
"content": "Full rewritten content with proper markdown formatting",
|
||||
"suggested_images": [
|
||||
{{
|
||||
"description": "Brief description of a suggested image",
|
||||
"caption": "Suggested caption for the image",
|
||||
"placement": "Where this image should be placed (e.g., 'After introduction', 'Before conclusion')"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
Ensure the JSON is properly formatted and valid.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
def rewrite_blog(self, original_content: Dict[str, Any],
|
||||
user_preferences: Dict[str, Any],
|
||||
research_results: Dict[str, Any],
|
||||
content_analysis: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Rewrite the blog based on original content, user preferences, and research.
|
||||
|
||||
Args:
|
||||
original_content: Original blog content
|
||||
user_preferences: User preferences for rewriting
|
||||
research_results: Research results for updating content
|
||||
content_analysis: Analysis of the original content
|
||||
|
||||
Returns:
|
||||
Dictionary containing rewritten content
|
||||
"""
|
||||
logger.info("Rewriting blog content")
|
||||
|
||||
# Generate the prompt
|
||||
prompt = self.generate_rewrite_prompt(
|
||||
original_content, user_preferences, research_results, content_analysis
|
||||
)
|
||||
|
||||
# Call the LLM to rewrite the content
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
|
||||
# Clean the response of any invalid control characters
|
||||
response = ''.join(char for char in response if ord(char) >= 32 or char in '\n\r\t')
|
||||
|
||||
# Extract JSON from the response
|
||||
json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL)
|
||||
if json_match:
|
||||
json_str = json_match.group(1)
|
||||
else:
|
||||
# If no JSON block found, try to find JSON-like content
|
||||
json_match = re.search(r'\{.*\}', response, re.DOTALL)
|
||||
if json_match:
|
||||
json_str = json_match.group(0)
|
||||
else:
|
||||
json_str = response
|
||||
|
||||
# Clean up the JSON string
|
||||
json_str = re.sub(r'```(json)?', '', json_str).strip()
|
||||
|
||||
# Remove any remaining invalid control characters
|
||||
json_str = ''.join(char for char in json_str if ord(char) >= 32 or char in '\n\r\t')
|
||||
|
||||
# Parse the JSON with error handling
|
||||
try:
|
||||
rewritten_content = json.loads(json_str)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON parsing error: {e}")
|
||||
# Try to fix common JSON issues
|
||||
json_str = json_str.replace('\\n', '\\\\n') # Fix escaped newlines
|
||||
json_str = json_str.replace('\\"', '"') # Fix escaped quotes
|
||||
json_str = json_str.replace('\\t', '\\\\t') # Fix escaped tabs
|
||||
rewritten_content = json.loads(json_str)
|
||||
|
||||
# Validate the response structure
|
||||
required_fields = ["title", "meta_description", "content"]
|
||||
for field in required_fields:
|
||||
if field not in rewritten_content:
|
||||
rewritten_content[field] = original_content.get(field, "")
|
||||
logger.warning(f"Missing required field '{field}' in rewritten content")
|
||||
|
||||
# Ensure suggested_images exists
|
||||
if "suggested_images" not in rewritten_content:
|
||||
rewritten_content["suggested_images"] = []
|
||||
|
||||
# Clean up the content field
|
||||
if "content" in rewritten_content:
|
||||
# Remove any remaining invalid control characters
|
||||
rewritten_content["content"] = ''.join(
|
||||
char for char in rewritten_content["content"]
|
||||
if ord(char) >= 32 or char in '\n\r\t'
|
||||
)
|
||||
# Normalize whitespace
|
||||
rewritten_content["content"] = re.sub(r'\s+', ' ', rewritten_content["content"])
|
||||
rewritten_content["content"] = re.sub(r'\n{3,}', '\n\n', rewritten_content["content"])
|
||||
|
||||
return rewritten_content
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error rewriting blog: {e}")
|
||||
return {
|
||||
"title": original_content.get("title", ""),
|
||||
"meta_description": original_content.get("meta_description", ""),
|
||||
"content": original_content.get("content", ""),
|
||||
"suggested_images": [],
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def generate_image(self, image_prompt: str, style: str = "realistic") -> str:
|
||||
"""
|
||||
Generate an image based on the prompt.
|
||||
|
||||
Args:
|
||||
image_prompt: Prompt for image generation
|
||||
style: Style of the image
|
||||
|
||||
Returns:
|
||||
Path to the generated image
|
||||
"""
|
||||
logger.info(f"Generating image with prompt: {image_prompt}")
|
||||
|
||||
try:
|
||||
image_path = generate_image(image_prompt, style=style)
|
||||
return image_path
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating image: {e}")
|
||||
return ""
|
||||
@@ -1,47 +0,0 @@
|
||||
# AI Blog Metadata Generator
|
||||
|
||||
The AI Blog Metadata Generator module is designed to assist in creating SEO-optimized metadata for blog articles. Utilizing artificial intelligence, this module generates high-quality metadata to enhance the visibility and engagement of blog posts.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To use this module, ensure that the following prerequisites are met:
|
||||
|
||||
- Python 3.6 or higher
|
||||
- Streamlit
|
||||
- Loguru
|
||||
- Asyncio
|
||||
- A GPT provider (e.g., OpenAI, Gemini)
|
||||
|
||||
## Installation
|
||||
|
||||
Install the required packages using the Python package installer, pip:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Follow these steps to utilize the AI Blog Metadata Generator module:
|
||||
|
||||
### Generate Blog Title
|
||||
|
||||
The module provides a function to create a blog title that is both SEO-optimized and engaging. This function ensures the title adheres to SEO best practices and avoids negative keywords.
|
||||
|
||||
### Generate Meta Description
|
||||
|
||||
This functionality creates a compelling meta description for the blog content. The description is kept between 150-160 characters to ensure it meets SEO standards.
|
||||
|
||||
### Generate Blog Tags
|
||||
|
||||
The module suggests relevant and specific tags for the blog content. This helps in categorizing and improving the discoverability of the blog post.
|
||||
|
||||
### Generate Blog Categories
|
||||
|
||||
The module identifies the main topics and suggests the most relevant categories for the blog content. This function ensures that the blog is categorized appropriately for the target audience and taxonomy.
|
||||
|
||||
## Helper Functions
|
||||
|
||||
The module includes helper functions to run the asyncio event loop within Streamlit, ensuring smooth and efficient operation of asynchronous tasks such as generating metadata.
|
||||
|
||||
By leveraging this module, users can enhance their blog posts with well-crafted metadata, improving their visibility and engagement in search engines.
|
||||
@@ -1,435 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
import datetime
|
||||
import sys
|
||||
import streamlit as st
|
||||
from loguru import logger
|
||||
import random
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
|
||||
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
async def blog_metadata(blog_article):
|
||||
"""
|
||||
Generate comprehensive SEO metadata for a blog article.
|
||||
|
||||
Args:
|
||||
blog_article (str): The content of the blog article
|
||||
|
||||
Returns:
|
||||
tuple: (blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug)
|
||||
"""
|
||||
logger.info("Generating comprehensive blog metadata")
|
||||
|
||||
progress_bar = st.progress(0)
|
||||
total_steps = 6 # Increased steps for new metadata types
|
||||
status_container = st.empty()
|
||||
|
||||
try:
|
||||
# Step 1: Generate blog title
|
||||
status_container.info("Generating SEO-optimized blog title...")
|
||||
await asyncio.sleep(random.uniform(0.5, 1.5))
|
||||
blog_title = generate_blog_title(blog_article)
|
||||
progress_bar.progress(1 / total_steps)
|
||||
|
||||
# Step 2: Generate blog meta description
|
||||
status_container.info("Creating compelling meta description...")
|
||||
await asyncio.sleep(random.uniform(0.5, 1.5))
|
||||
blog_meta_desc = generate_blog_description(blog_article)
|
||||
progress_bar.progress(2 / total_steps)
|
||||
|
||||
# Step 3: Generate blog tags
|
||||
status_container.info("Extracting relevant blog tags...")
|
||||
await asyncio.sleep(random.uniform(0.5, 1.5))
|
||||
blog_tags = get_blog_tags(blog_article)
|
||||
progress_bar.progress(3 / total_steps)
|
||||
|
||||
# Step 4: Generate blog categories
|
||||
status_container.info("Identifying primary blog categories...")
|
||||
await asyncio.sleep(random.uniform(0.5, 1.5))
|
||||
blog_categories = get_blog_categories(blog_article)
|
||||
progress_bar.progress(4 / total_steps)
|
||||
|
||||
# Step 5: Generate social media hashtags
|
||||
status_container.info("Creating social media hashtags...")
|
||||
await asyncio.sleep(random.uniform(0.5, 1.5))
|
||||
blog_hashtags = generate_blog_hashtags(blog_article)
|
||||
progress_bar.progress(5 / total_steps)
|
||||
|
||||
# Step 6: Generate SEO URL slug
|
||||
status_container.info("Generating SEO-friendly URL slug...")
|
||||
await asyncio.sleep(random.uniform(0.5, 1.5))
|
||||
blog_slug = generate_blog_slug(blog_title)
|
||||
progress_bar.progress(6 / total_steps)
|
||||
|
||||
# Present the result in a table format
|
||||
status_container.success("✅ Blog SEO Metadata generation complete")
|
||||
#st.table({
|
||||
# "Metadata": ["Blog Title", "Meta Description", "Tags", "Categories", "Social Hashtags", "URL Slug"],
|
||||
# "Value": [blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug]
|
||||
#})
|
||||
|
||||
return blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug
|
||||
|
||||
except Exception as e:
|
||||
status_container.error(f"Error generating metadata: {str(e)}")
|
||||
logger.error(f"Failed to generate metadata: {str(e)}")
|
||||
# Return default values to ensure the blog generation process can continue
|
||||
return f"Blog Article", "An informative blog post", "content, blog", "General, Information", "#content #blog", "blog-article"
|
||||
|
||||
|
||||
def generate_blog_title(blog_article):
|
||||
"""
|
||||
Generate an SEO-optimized and engaging title for a blog article.
|
||||
|
||||
Args:
|
||||
blog_article (str): The content of the blog article
|
||||
|
||||
Returns:
|
||||
str: An SEO-optimized title
|
||||
"""
|
||||
logger.info("Generating SEO-optimized blog title")
|
||||
|
||||
# Extract the first 3000 characters for title generation
|
||||
snippet = blog_article[:3000] if len(blog_article) > 3000 else blog_article
|
||||
|
||||
prompt = f"""As an expert SEO copywriter, create the perfect blog title based on this content.
|
||||
|
||||
REQUIREMENTS:
|
||||
1. Make it compelling, specific, and actionable
|
||||
2. Include primary keywords naturally near the beginning
|
||||
3. Keep it between 50-60 characters (10-12 words maximum)
|
||||
4. Make it promise clear value to the reader
|
||||
5. Use power words that evoke emotion where appropriate
|
||||
|
||||
AVOID:
|
||||
- Clickbait tactics or false promises
|
||||
- Generic titles that could apply to any article
|
||||
- Using words like "unveiling", "unleash", "power of", "ultimate guide", or "complete"
|
||||
- ALL CAPS or excessive punctuation!!!!
|
||||
|
||||
EXAMPLES OF GREAT TITLES:
|
||||
- "7 Proven Strategies to Improve Your Email Marketing ROI"
|
||||
- "Why Remote Work Improves Productivity: New Research Findings"
|
||||
- "How to Build a Personal Budget That Actually Works"
|
||||
|
||||
CONTENT TO ANALYZE:
|
||||
"{snippet}"
|
||||
|
||||
Reply with ONLY the title and no other text or explanation.
|
||||
"""
|
||||
try:
|
||||
title = llm_text_gen(prompt)
|
||||
# Clean up any quotes or extra spaces
|
||||
title = title.strip('"\'').strip()
|
||||
logger.info(f"Generated title: {title}")
|
||||
return title
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to generate blog title: {err}")
|
||||
return "Blog Article" # Fallback title
|
||||
|
||||
|
||||
def generate_blog_description(blog_content):
|
||||
"""
|
||||
Generate an SEO-optimized meta description for the blog.
|
||||
|
||||
Args:
|
||||
blog_content (str): The content of the blog article
|
||||
|
||||
Returns:
|
||||
str: An SEO-optimized meta description
|
||||
"""
|
||||
logger.info("Generating SEO-optimized meta description")
|
||||
|
||||
# Extract the first 2000 characters for description generation
|
||||
snippet = blog_content[:2000] if len(blog_content) > 2000 else blog_content
|
||||
|
||||
prompt = f"""As an SEO expert, write the perfect meta description for this blog content.
|
||||
|
||||
REQUIREMENTS:
|
||||
1. Exactly 150-160 characters (this is critical for SEO)
|
||||
2. Include primary keywords naturally
|
||||
3. Compelling value proposition that makes readers want to click
|
||||
4. Clear indication of what the reader will learn/gain
|
||||
5. End with an implicit call-to-action when possible
|
||||
|
||||
EXAMPLES OF EXCELLENT META DESCRIPTIONS:
|
||||
- "Learn how to increase email open rates by 43% with these 5 proven strategies from industry experts. Implement today for immediate results."
|
||||
- "Discover why 67% of professionals struggle with work-life balance and explore research-backed techniques to reclaim your time and energy."
|
||||
|
||||
CONTENT TO SUMMARIZE:
|
||||
"{snippet}"
|
||||
|
||||
Reply with ONLY the meta description and no other text. Keep it between 150-160 characters exactly.
|
||||
"""
|
||||
try:
|
||||
description = llm_text_gen(prompt)
|
||||
# Clean up any quotes or extra spaces
|
||||
description = description.strip('"\'').strip()
|
||||
logger.info(f"Generated meta description: {description}")
|
||||
return description
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to generate blog description: {err}")
|
||||
return "An informative blog post about this topic." # Fallback description
|
||||
|
||||
|
||||
def get_blog_tags(blog_article):
|
||||
"""
|
||||
Generate relevant SEO tags for a blog article.
|
||||
|
||||
Args:
|
||||
blog_article (str): The content of the blog article
|
||||
|
||||
Returns:
|
||||
str: Comma-separated list of relevant tags
|
||||
"""
|
||||
logger.info("Generating SEO-optimized blog tags")
|
||||
|
||||
# Extract the first 3000 characters for tag generation
|
||||
snippet = blog_article[:3000] if len(blog_article) > 3000 else blog_article
|
||||
|
||||
prompt = f"""As an SEO specialist, extract the 4-6 most relevant tags for this blog post.
|
||||
|
||||
REQUIREMENTS:
|
||||
1. Choose specific, targeted keywords that accurately represent the content
|
||||
2. Include a mix of broad and specific tags
|
||||
3. Focus on terms users would actually search for
|
||||
4. Include at least one long-tail keyword phrase
|
||||
5. Ensure all tags are directly addressed in the content
|
||||
|
||||
CONTENT TO ANALYZE:
|
||||
"{snippet}"
|
||||
|
||||
Reply with ONLY the tags as a comma-separated list (e.g., "keyword1, keyword2, keyword3, keyword phrase"). Provide 4-6 tags total.
|
||||
"""
|
||||
try:
|
||||
tags = llm_text_gen(prompt)
|
||||
# Clean up any quotes or extra commas
|
||||
tags = tags.strip('"\'').strip()
|
||||
if tags.endswith(','):
|
||||
tags = tags[:-1]
|
||||
logger.info(f"Generated tags: {tags}")
|
||||
return tags
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to generate blog tags: {err}")
|
||||
return "content, blog" # Fallback tags
|
||||
|
||||
|
||||
def get_blog_categories(blog_article):
|
||||
"""
|
||||
Identify the most appropriate blog categories for the article.
|
||||
|
||||
Args:
|
||||
blog_article (str): The content of the blog article
|
||||
|
||||
Returns:
|
||||
str: Comma-separated list of relevant categories
|
||||
"""
|
||||
logger.info("Generating blog categories")
|
||||
|
||||
# Extract the first 2000 characters for category generation
|
||||
snippet = blog_article[:2000] if len(blog_article) > 2000 else blog_article
|
||||
|
||||
prompt = f"""As a content strategist, identify the 2-3 most appropriate high-level categories for this blog.
|
||||
|
||||
REQUIREMENTS:
|
||||
1. Choose broad, established categories used in content organization
|
||||
2. Select categories that best represent the main themes of the article
|
||||
3. Consider the target audience and their interests
|
||||
4. Focus on categories that would help with site navigation
|
||||
5. Aim for a primary category and 1-2 supporting categories
|
||||
|
||||
EXAMPLES OF GOOD CATEGORIES:
|
||||
- Marketing, Social Media, Strategy
|
||||
- Finance, Personal Budgeting, Money Management
|
||||
- Productivity, Remote Work, Business
|
||||
|
||||
CONTENT TO ANALYZE:
|
||||
"{snippet}"
|
||||
|
||||
Reply with ONLY the categories as a comma-separated list (e.g., "Category1, Category2, Category3"). Provide 2-3 categories total.
|
||||
"""
|
||||
try:
|
||||
categories = llm_text_gen(prompt)
|
||||
# Clean up any quotes or extra commas
|
||||
categories = categories.strip('"\'').strip()
|
||||
if categories.endswith(','):
|
||||
categories = categories[:-1]
|
||||
logger.info(f"Generated categories: {categories}")
|
||||
return categories
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to generate blog categories: {err}")
|
||||
return "General, Information" # Fallback categories
|
||||
|
||||
|
||||
def generate_blog_hashtags(blog_article):
|
||||
"""
|
||||
Generate social media hashtags for promoting the blog article.
|
||||
|
||||
Args:
|
||||
blog_article (str): The content of the blog article
|
||||
|
||||
Returns:
|
||||
str: Space-separated list of hashtags starting with #
|
||||
"""
|
||||
logger.info("Generating social media hashtags")
|
||||
|
||||
# Extract the first 2000 characters for hashtag generation
|
||||
snippet = blog_article[:2000] if len(blog_article) > 2000 else blog_article
|
||||
|
||||
prompt = f"""As a social media strategist, create 5-7 effective hashtags for this blog content.
|
||||
|
||||
REQUIREMENTS:
|
||||
1. Mix of popular and niche hashtags for better visibility
|
||||
2. Include industry-specific and trending hashtags where relevant
|
||||
3. Avoid overly generic hashtags (like #content or #blog)
|
||||
4. Format each hashtag with # symbol and camelCase or separate words
|
||||
5. Include at least one branded or campaign-style hashtag
|
||||
|
||||
EXAMPLES OF EFFECTIVE HASHTAG SETS:
|
||||
- #EmailMarketing #ROITips #DigitalStrategy #MarketingTips #GrowthHacking #EmailROI
|
||||
- #RemoteWork #ProductivityTips #FutureOfWork #WorkFromHome #RemoteProductivity #HRInsights
|
||||
|
||||
CONTENT TO ANALYZE:
|
||||
"{snippet}"
|
||||
|
||||
Reply with ONLY the hashtags, each starting with # and separated by spaces. Provide 5-7 hashtags total.
|
||||
"""
|
||||
try:
|
||||
hashtags = llm_text_gen(prompt)
|
||||
# Clean up any quotes or extra spaces
|
||||
hashtags = hashtags.strip('"\'').strip()
|
||||
# Ensure all hashtags start with #
|
||||
if not hashtags.startswith('#'):
|
||||
hashtags = ' '.join([f"#{tag.strip('#')}" for tag in hashtags.split()])
|
||||
logger.info(f"Generated hashtags: {hashtags}")
|
||||
return hashtags
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to generate blog hashtags: {err}")
|
||||
return "#content #blog" # Fallback hashtags
|
||||
|
||||
|
||||
def generate_blog_slug(blog_title):
|
||||
"""
|
||||
Generate an SEO-friendly URL slug from the blog title.
|
||||
|
||||
Args:
|
||||
blog_title (str): The title of the blog article
|
||||
|
||||
Returns:
|
||||
str: An SEO-friendly URL slug
|
||||
"""
|
||||
logger.info("Generating SEO-friendly URL slug")
|
||||
|
||||
try:
|
||||
# Use a prompt to generate a customized slug
|
||||
prompt = f"""As an SEO specialist, create an SEO-friendly URL slug for this blog title: "{blog_title}"
|
||||
|
||||
REQUIREMENTS:
|
||||
1. Keep it under 60 characters
|
||||
2. Use only lowercase letters, numbers, and hyphens
|
||||
3. Include primary keywords near the beginning
|
||||
4. Remove all unnecessary words (a, the, and, or, but, etc.)
|
||||
5. Ensure it's human-readable and descriptive
|
||||
|
||||
EXAMPLES:
|
||||
- Title: "10 Effective Ways to Improve Your Email Marketing ROI This Quarter"
|
||||
Slug: "improve-email-marketing-roi"
|
||||
|
||||
- Title: "Why Most Remote Workers Are More Productive According to New Research"
|
||||
Slug: "remote-workers-productivity-research"
|
||||
|
||||
Reply with ONLY the slug and no other text or explanation.
|
||||
"""
|
||||
slug = llm_text_gen(prompt)
|
||||
|
||||
# Clean up and normalize the slug
|
||||
slug = slug.strip('"\'').strip()
|
||||
|
||||
# If the LLM didn't create a proper slug, do it programmatically
|
||||
if not re.match(r'^[a-z0-9-]+$', slug):
|
||||
# Fallback to simple programmatic slug creation
|
||||
slug = blog_title.lower()
|
||||
# Remove special characters
|
||||
slug = re.sub(r'[^a-z0-9\s-]', '', slug)
|
||||
# Replace spaces with hyphens
|
||||
slug = re.sub(r'\s+', '-', slug)
|
||||
# Remove redundant hyphens
|
||||
slug = re.sub(r'-+', '-', slug)
|
||||
# Limit length to 60 characters
|
||||
slug = slug[:60].strip('-')
|
||||
|
||||
logger.info(f"Generated slug: {slug}")
|
||||
return slug
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to generate blog slug: {err}")
|
||||
# Create a simple slug programmatically as fallback
|
||||
slug = blog_title.lower()
|
||||
slug = re.sub(r'[^a-z0-9\s-]', '', slug)
|
||||
slug = re.sub(r'\s+', '-', slug)
|
||||
slug = re.sub(r'-+', '-', slug)
|
||||
slug = slug[:60].strip('-')
|
||||
return slug
|
||||
|
||||
|
||||
# Helper function to run the asyncio event loop within Streamlit
|
||||
def run_async(coro):
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
result = loop.run_until_complete(coro)
|
||||
loop.close()
|
||||
return result
|
||||
|
||||
|
||||
def get_blog_metadata_longform(longform_content):
|
||||
""" Function for caching long-form content """
|
||||
# Open the file in write mode ("w") to overwrite existing content.
|
||||
filepath = os.path.join(os.getenv("CONTENT_SAVE_DIR"), "lognform_metadata_file")
|
||||
with open(filepath, "w") as file:
|
||||
# Write the text to the file
|
||||
file.write(longform_content)
|
||||
print(f"String saved successfully to: {filepath}")
|
||||
|
||||
#genai.configure(api_key=os.environ['GEMINI_API_KEY'])
|
||||
#file_path = genai.upload_file(path=filepath)
|
||||
|
||||
# Wait for the file to finish processing
|
||||
#while file_path.state.name == 'PROCESSING':
|
||||
# print('Waiting for video to be processed.')
|
||||
# time.sleep(2)
|
||||
# file_path = genai.get_file(video_file.name)
|
||||
|
||||
#print(f'Video processing complete: {file_path.uri}')
|
||||
|
||||
# Create a cache with a 5 minute TTL
|
||||
#cache = caching.CachedContent.create(
|
||||
# model='models/gemini-1.5-flash-001',
|
||||
# display_name='Alwrity Longform content', # used to identify the cache
|
||||
# system_instruction=(
|
||||
# 'You are an expert file analyzer , and your job is to answer '
|
||||
# 'the user\'s query based on the file you have access to.'
|
||||
# ),
|
||||
# contents=[file_path],
|
||||
# ttl=datetime.timedelta(minutes=15),
|
||||
#)
|
||||
|
||||
# Construct a GenerativeModel which uses the created cache.
|
||||
#model = genai.GenerativeModel.from_cached_content(cached_content=cache)
|
||||
|
||||
# Query the model
|
||||
#response = model.generate_content([(
|
||||
# 'SUmmarize the given file '
|
||||
# 'in 10 lines '
|
||||
# 'list main points')])
|
||||
|
||||
#print(response.usage_metadata)
|
||||
#return(response.text)
|
||||
return("TBD: Not implemented")
|
||||
@@ -1,222 +0,0 @@
|
||||
# Enhanced ALwrity Chatbot
|
||||
|
||||
An intelligent conversational AI assistant that transforms content creation, SEO analysis, and workflow automation through advanced AI-powered interactions.
|
||||
|
||||
## 🚀 Major Enhancements
|
||||
|
||||
### **Before vs After Transformation**
|
||||
|
||||
| **Before** | **After** |
|
||||
|------------|-----------|
|
||||
| Basic RAG chatbot | Intelligent workflow-driven assistant |
|
||||
| Simple Q&A interface | Context-aware conversational AI |
|
||||
| Manual tool selection | Smart intent analysis & tool routing |
|
||||
| Static responses | Dynamic, personalized interactions |
|
||||
| Limited functionality | Comprehensive content creation hub |
|
||||
|
||||
## 🎯 Key Improvements
|
||||
|
||||
### 1. **Smart Intent Analysis & Tool Routing**
|
||||
*Impact: High | Complexity: High*
|
||||
- **Enhanced Intent Detection**: Advanced NLP analysis of user queries
|
||||
- **Confidence Scoring**: Reliability metrics for intent predictions
|
||||
- **Context-Aware Routing**: Intelligent tool selection based on conversation history
|
||||
- **Multi-Intent Handling**: Process complex requests with multiple objectives
|
||||
|
||||
### 2. **Workflow Automation Engine**
|
||||
*Impact: High | Complexity: High*
|
||||
- **Pre-built Workflows**: Ready-to-use processes for common tasks
|
||||
- **Custom Workflow Creation**: Build personalized automation sequences
|
||||
- **Progress Tracking**: Visual workflow progress with step-by-step guidance
|
||||
- **Smart Step Guidance**: Context-aware assistance at each workflow stage
|
||||
|
||||
### 3. **Real-Time Analysis Integration**
|
||||
*Impact: High | Complexity: High*
|
||||
- **Instant URL Analysis**: Real-time SEO and content analysis
|
||||
- **Live SEO Scoring**: Dynamic website performance metrics
|
||||
- **Content Gap Detection**: Automated competitive analysis
|
||||
- **Technical SEO Alerts**: Proactive issue identification
|
||||
|
||||
### 4. **Enhanced AI Prompts & Context System**
|
||||
*Impact: High | Complexity: High*
|
||||
- **Advanced System Prompts**: Specialized prompts for different content types
|
||||
- **Comprehensive Context Building**: Multi-layered conversation understanding
|
||||
- **Dynamic Response Structures**: Adaptive formatting based on user needs
|
||||
- **Smart Follow-up Generation**: Intelligent conversation continuation
|
||||
|
||||
### 5. **Modular UI Components** ⭐ *NEW*
|
||||
*Impact: High | Complexity: Medium*
|
||||
- **Intelligent Sidebar Manager**: Organized dashboard with smart features
|
||||
- **Component-Based Architecture**: Reusable UI elements for maintainability
|
||||
- **Responsive Design**: Optimized interface for different screen sizes
|
||||
- **State Management**: Persistent UI preferences and interactions
|
||||
|
||||
### 6. **Intelligent Sidebar Hub**
|
||||
*Impact: Medium | Complexity: Medium*
|
||||
- **Smart Dashboard**: Real-time metrics and usage analytics
|
||||
- **Quick Tools Access**: One-click access to frequently used features
|
||||
- **Organized Categories**: Intuitive grouping of tools and workflows
|
||||
- **User Preferences**: Customizable interface and content settings
|
||||
|
||||
### 7. **Content Workspace Management**
|
||||
*Impact: Medium | Complexity: Medium*
|
||||
- **Draft System**: Save and manage work-in-progress content
|
||||
- **Workspace Export**: Multiple format export options (JSON, TXT, etc.)
|
||||
- **Content Ideas Generator**: AI-powered content suggestions
|
||||
- **Session Management**: Persistent conversation and workspace state
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
lib/chatbot_custom/
|
||||
├── enhanced_alwrity_chatbot.py # Main enhanced chatbot (1,783 lines)
|
||||
├── enhanced_alwrity_chatbot_modular.py # Modular version with UI components
|
||||
├── ui/ # UI Components Module
|
||||
│ ├── __init__.py # UI package initialization
|
||||
│ └── sidebar.py # Sidebar Manager component
|
||||
├── README.md # This comprehensive documentation
|
||||
├── SETUP.md # Setup and configuration guide
|
||||
└── ENHANCEMENT_SUMMARY.md # Detailed enhancement summary
|
||||
```
|
||||
|
||||
## 🔧 Installation
|
||||
|
||||
The enhanced chatbot uses existing ALwrity dependencies. Install all requirements from the project root:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
> **Note**: All required dependencies are already included in the main project `requirements.txt`. No additional packages needed.
|
||||
|
||||
## ⚙️ Environment Variables
|
||||
|
||||
Create a `.env` file in the project root with your API keys:
|
||||
|
||||
```env
|
||||
OPENAI_API_KEY=your_openai_api_key
|
||||
GOOGLE_API_KEY=your_google_api_key
|
||||
ANTHROPIC_API_KEY=your_anthropic_api_key
|
||||
SERPER_API_KEY=your_serper_api_key
|
||||
```
|
||||
|
||||
## 🚀 Running the Chatbot
|
||||
|
||||
### Standard Version
|
||||
```bash
|
||||
streamlit run lib/chatbot_custom/enhanced_alwrity_chatbot.py
|
||||
```
|
||||
|
||||
### Modular Version (Recommended)
|
||||
```bash
|
||||
streamlit run lib/chatbot_custom/enhanced_alwrity_chatbot_modular.py
|
||||
```
|
||||
|
||||
## 💻 Usage Examples
|
||||
|
||||
### Smart Tool Routing
|
||||
```python
|
||||
# User input: "I need to analyze my competitor's website"
|
||||
# System automatically:
|
||||
# 1. Detects intent: competitor analysis
|
||||
# 2. Routes to: website analyzer + competitor tools
|
||||
# 3. Provides: comprehensive competitive analysis
|
||||
```
|
||||
|
||||
### Real-Time Analysis Integration
|
||||
```python
|
||||
# User input: "Check the SEO of https://example.com"
|
||||
# System provides:
|
||||
# - Technical SEO analysis
|
||||
# - Content gap analysis
|
||||
# - On-page optimization suggestions
|
||||
# - Competitor comparison
|
||||
```
|
||||
|
||||
### Workflow Automation
|
||||
```python
|
||||
# Blog Creation Workflow:
|
||||
# Step 1: Topic research and keyword analysis
|
||||
# Step 2: Content outline generation
|
||||
# Step 3: SEO optimization suggestions
|
||||
# Step 4: Content creation with AI assistance
|
||||
# Step 5: Final review and export options
|
||||
```
|
||||
|
||||
## 🔄 Workflow Examples
|
||||
|
||||
### **Blog Creation Workflow**
|
||||
1. **Research Phase**: Keyword analysis and competitor research
|
||||
2. **Planning Phase**: Content outline and structure creation
|
||||
3. **Creation Phase**: AI-assisted content generation
|
||||
4. **Optimization Phase**: SEO enhancement and refinement
|
||||
5. **Publishing Phase**: Final review and export options
|
||||
|
||||
### **Competitor Analysis Workflow**
|
||||
1. **Discovery Phase**: Identify key competitors and URLs
|
||||
2. **Analysis Phase**: Technical SEO and content analysis
|
||||
3. **Comparison Phase**: Gap analysis and opportunities
|
||||
4. **Strategy Phase**: Actionable recommendations
|
||||
5. **Reporting Phase**: Comprehensive analysis export
|
||||
|
||||
## 🎨 User Experience Improvements
|
||||
|
||||
- **Intuitive Interface**: Clean, modern design with logical information hierarchy
|
||||
- **Smart Suggestions**: Context-aware tool and workflow recommendations
|
||||
- **Visual Progress Tracking**: Clear workflow progress indicators
|
||||
- **Personalized Experience**: Adaptive interface based on user preferences
|
||||
- **Efficient Navigation**: Quick access to frequently used features
|
||||
- **Comprehensive Help**: Contextual guidance and documentation
|
||||
|
||||
## 📊 Performance Metrics
|
||||
|
||||
- **🎯 100% ALwrity Tool Integration**: Seamless access to all ALwrity features
|
||||
- **⚡ 3x Workflow Efficiency**: Automated processes reduce manual steps
|
||||
- **🧠 5x Smarter Responses**: Context-aware AI with advanced prompting
|
||||
- **📈 Real-time Analysis**: Instant SEO and content insights
|
||||
- **🎨 Enhanced UI/UX**: Modern, intuitive interface design
|
||||
|
||||
## 🔮 Future Enhancements
|
||||
|
||||
- **Multi-language Support**: Content creation in multiple languages
|
||||
- **Advanced Analytics Dashboard**: Comprehensive usage and performance metrics
|
||||
- **Team Collaboration Features**: Shared workspaces and collaborative editing
|
||||
- **API Integration**: External tool connections and data synchronization
|
||||
- **Mobile Optimization**: Enhanced mobile experience and responsive design
|
||||
- **Voice Interface**: Speech-to-text and voice commands
|
||||
- **Plugin System**: Extensible architecture for custom integrations
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions to enhance the ALwrity chatbot further!
|
||||
|
||||
### Steps to Contribute:
|
||||
1. **Fork the Repository**: Create your own copy of the project
|
||||
2. **Create Feature Branch**: `git checkout -b feature/AmazingFeature`
|
||||
3. **Commit Changes**: `git commit -m 'Add AmazingFeature'`
|
||||
4. **Push to Branch**: `git push origin feature/AmazingFeature`
|
||||
5. **Open Pull Request**: Submit your changes for review
|
||||
|
||||
### Development Guidelines:
|
||||
- Follow existing code style and conventions
|
||||
- Add comprehensive documentation for new features
|
||||
- Include unit tests for new functionality
|
||||
- Ensure compatibility with existing ALwrity tools
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- **[Setup Guide](SETUP.md)**: Detailed installation and configuration instructions
|
||||
- **[Enhancement Summary](ENHANCEMENT_SUMMARY.md)**: Comprehensive overview of improvements
|
||||
- **[ALwrity Documentation](../../README.md)**: Main project documentation
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
- **GitHub Issues**: [Report bugs or request features](https://github.com/AJaySi/AI-Writer/issues)
|
||||
- **Documentation**: Comprehensive guides and API references
|
||||
- **Community**: Join discussions and get help from other users
|
||||
|
||||
---
|
||||
|
||||
**🎉 Experience the power of intelligent content creation with Enhanced ALwrity!**
|
||||
|
||||
*Transform your content workflow with AI-driven automation, real-time analysis, and intelligent assistance.*
|
||||
@@ -1,21 +0,0 @@
|
||||
"""
|
||||
Core modules for the Enhanced ALwrity Chatbot.
|
||||
|
||||
This package contains the core functionality split into manageable modules:
|
||||
- workflow_engine: Handles multi-tool workflows and automation
|
||||
- tool_router: Intelligent tool routing based on user intent
|
||||
- intent_analyzer: Advanced user intent analysis
|
||||
- context_manager: Conversation context and state management
|
||||
"""
|
||||
|
||||
from .workflow_engine import WorkflowEngine
|
||||
from .tool_router import SmartToolRouter
|
||||
from .intent_analyzer import IntentAnalyzer
|
||||
from .context_manager import ContextManager
|
||||
|
||||
__all__ = [
|
||||
'WorkflowEngine',
|
||||
'SmartToolRouter',
|
||||
'IntentAnalyzer',
|
||||
'ContextManager'
|
||||
]
|
||||
@@ -1,413 +0,0 @@
|
||||
"""
|
||||
Context Manager for Enhanced ALwrity Chatbot.
|
||||
|
||||
Manages conversation context, state, and user preferences with persistence.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConversationTurn:
|
||||
"""Represents a single conversation turn."""
|
||||
timestamp: str
|
||||
user_input: str
|
||||
intent: str
|
||||
tools_used: List[str]
|
||||
response_summary: str
|
||||
satisfaction_score: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserPreferences:
|
||||
"""User preferences and settings."""
|
||||
content_preferences: List[str]
|
||||
preferred_tone: str
|
||||
preferred_length: str
|
||||
industry_focus: List[str]
|
||||
language: str
|
||||
timezone: str
|
||||
notification_settings: Dict[str, bool]
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowState:
|
||||
"""Represents the state of an active workflow."""
|
||||
workflow_id: str
|
||||
workflow_name: str
|
||||
current_step: int
|
||||
total_steps: int
|
||||
step_data: Dict[str, Any]
|
||||
started_at: str
|
||||
last_updated: str
|
||||
is_paused: bool = False
|
||||
|
||||
|
||||
class ContextManager:
|
||||
"""Advanced conversation context and state management."""
|
||||
|
||||
def __init__(self, user_id: str = "default", context_file: str = None):
|
||||
self.user_id = user_id
|
||||
self.context_file = context_file or f"user_context_{user_id}.json"
|
||||
self.context_dir = "lib/chatbot_custom/user_contexts"
|
||||
|
||||
# Ensure context directory exists
|
||||
os.makedirs(self.context_dir, exist_ok=True)
|
||||
self.context_path = os.path.join(self.context_dir, self.context_file)
|
||||
|
||||
# Initialize context data
|
||||
self.conversation_history: List[ConversationTurn] = []
|
||||
self.user_preferences: UserPreferences = UserPreferences(
|
||||
content_preferences=[],
|
||||
preferred_tone="professional",
|
||||
preferred_length="medium",
|
||||
industry_focus=[],
|
||||
language="en",
|
||||
timezone="UTC",
|
||||
notification_settings={}
|
||||
)
|
||||
self.active_workflows: List[WorkflowState] = []
|
||||
self.tool_usage_history: List[Dict[str, Any]] = []
|
||||
self.session_data: Dict[str, Any] = {}
|
||||
self.analytics_data: Dict[str, Any] = {
|
||||
"total_interactions": 0,
|
||||
"tools_used_count": {},
|
||||
"workflows_completed": 0,
|
||||
"average_session_length": 0,
|
||||
"last_active": None
|
||||
}
|
||||
|
||||
# Load existing context
|
||||
self.load_context()
|
||||
|
||||
def add_conversation_turn(self, user_input: str, intent: str,
|
||||
tools_used: List[str], response_summary: str,
|
||||
satisfaction_score: Optional[float] = None):
|
||||
"""Add a new conversation turn to history."""
|
||||
turn = ConversationTurn(
|
||||
timestamp=datetime.now().isoformat(),
|
||||
user_input=user_input,
|
||||
intent=intent,
|
||||
tools_used=tools_used,
|
||||
response_summary=response_summary,
|
||||
satisfaction_score=satisfaction_score
|
||||
)
|
||||
|
||||
self.conversation_history.append(turn)
|
||||
|
||||
# Keep only last 50 turns to manage memory
|
||||
if len(self.conversation_history) > 50:
|
||||
self.conversation_history = self.conversation_history[-50:]
|
||||
|
||||
# Update analytics
|
||||
self.analytics_data["total_interactions"] += 1
|
||||
self.analytics_data["last_active"] = datetime.now().isoformat()
|
||||
|
||||
# Update tool usage statistics
|
||||
for tool in tools_used:
|
||||
if tool in self.analytics_data["tools_used_count"]:
|
||||
self.analytics_data["tools_used_count"][tool] += 1
|
||||
else:
|
||||
self.analytics_data["tools_used_count"][tool] = 1
|
||||
|
||||
self.save_context()
|
||||
|
||||
def update_user_preferences(self, preferences: Dict[str, Any]):
|
||||
"""Update user preferences."""
|
||||
for key, value in preferences.items():
|
||||
if hasattr(self.user_preferences, key):
|
||||
setattr(self.user_preferences, key, value)
|
||||
|
||||
self.save_context()
|
||||
|
||||
def get_recent_context(self, turns: int = 5) -> List[ConversationTurn]:
|
||||
"""Get recent conversation turns for context."""
|
||||
return self.conversation_history[-turns:] if self.conversation_history else []
|
||||
|
||||
def get_recent_topics(self, hours: int = 24) -> List[str]:
|
||||
"""Get topics discussed in recent hours."""
|
||||
cutoff_time = datetime.now() - timedelta(hours=hours)
|
||||
recent_topics = []
|
||||
|
||||
for turn in self.conversation_history:
|
||||
turn_time = datetime.fromisoformat(turn.timestamp)
|
||||
if turn_time > cutoff_time:
|
||||
# Extract topics from intent and tools used
|
||||
recent_topics.append(turn.intent)
|
||||
recent_topics.extend(turn.tools_used)
|
||||
|
||||
# Return unique topics
|
||||
return list(set(recent_topics))
|
||||
|
||||
def get_tool_usage_history(self, limit: int = 10) -> List[str]:
|
||||
"""Get recent tool usage history."""
|
||||
recent_tools = []
|
||||
for turn in self.conversation_history[-limit:]:
|
||||
recent_tools.extend(turn.tools_used)
|
||||
|
||||
return recent_tools
|
||||
|
||||
def start_workflow(self, workflow_id: str, workflow_name: str, total_steps: int):
|
||||
"""Start a new workflow."""
|
||||
workflow_state = WorkflowState(
|
||||
workflow_id=workflow_id,
|
||||
workflow_name=workflow_name,
|
||||
current_step=0,
|
||||
total_steps=total_steps,
|
||||
step_data={},
|
||||
started_at=datetime.now().isoformat(),
|
||||
last_updated=datetime.now().isoformat()
|
||||
)
|
||||
|
||||
self.active_workflows.append(workflow_state)
|
||||
self.save_context()
|
||||
|
||||
return workflow_state
|
||||
|
||||
def update_workflow_step(self, workflow_id: str, step_data: Dict[str, Any]):
|
||||
"""Update workflow step data."""
|
||||
for workflow in self.active_workflows:
|
||||
if workflow.workflow_id == workflow_id:
|
||||
workflow.current_step += 1
|
||||
workflow.step_data.update(step_data)
|
||||
workflow.last_updated = datetime.now().isoformat()
|
||||
|
||||
# Check if workflow is completed
|
||||
if workflow.current_step >= workflow.total_steps:
|
||||
self.complete_workflow(workflow_id)
|
||||
|
||||
self.save_context()
|
||||
return workflow
|
||||
|
||||
return None
|
||||
|
||||
def complete_workflow(self, workflow_id: str):
|
||||
"""Mark workflow as completed and remove from active workflows."""
|
||||
self.active_workflows = [w for w in self.active_workflows if w.workflow_id != workflow_id]
|
||||
self.analytics_data["workflows_completed"] += 1
|
||||
self.save_context()
|
||||
|
||||
def pause_workflow(self, workflow_id: str):
|
||||
"""Pause an active workflow."""
|
||||
for workflow in self.active_workflows:
|
||||
if workflow.workflow_id == workflow_id:
|
||||
workflow.is_paused = True
|
||||
workflow.last_updated = datetime.now().isoformat()
|
||||
self.save_context()
|
||||
return True
|
||||
return False
|
||||
|
||||
def resume_workflow(self, workflow_id: str):
|
||||
"""Resume a paused workflow."""
|
||||
for workflow in self.active_workflows:
|
||||
if workflow.workflow_id == workflow_id:
|
||||
workflow.is_paused = False
|
||||
workflow.last_updated = datetime.now().isoformat()
|
||||
self.save_context()
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_active_workflows(self) -> List[WorkflowState]:
|
||||
"""Get all active workflows."""
|
||||
return [w for w in self.active_workflows if not w.is_paused]
|
||||
|
||||
def get_paused_workflows(self) -> List[WorkflowState]:
|
||||
"""Get all paused workflows."""
|
||||
return [w for w in self.active_workflows if w.is_paused]
|
||||
|
||||
def set_session_data(self, key: str, value: Any):
|
||||
"""Set session-specific data."""
|
||||
self.session_data[key] = value
|
||||
|
||||
def get_session_data(self, key: str, default: Any = None) -> Any:
|
||||
"""Get session-specific data."""
|
||||
return self.session_data.get(key, default)
|
||||
|
||||
def clear_session_data(self):
|
||||
"""Clear all session data."""
|
||||
self.session_data.clear()
|
||||
|
||||
def get_context_for_intent_analysis(self) -> Dict[str, Any]:
|
||||
"""Get context data for intent analysis."""
|
||||
return {
|
||||
"recent_topics": self.get_recent_topics(),
|
||||
"user_preferences": asdict(self.user_preferences),
|
||||
"active_workflows": [w.workflow_name for w in self.get_active_workflows()],
|
||||
"tool_usage_history": self.get_tool_usage_history(),
|
||||
"session_data": self.session_data
|
||||
}
|
||||
|
||||
def get_user_analytics(self) -> Dict[str, Any]:
|
||||
"""Get user analytics and usage statistics."""
|
||||
# Calculate average session length
|
||||
if self.conversation_history:
|
||||
session_starts = []
|
||||
current_session_start = None
|
||||
|
||||
for turn in self.conversation_history:
|
||||
turn_time = datetime.fromisoformat(turn.timestamp)
|
||||
if not current_session_start:
|
||||
current_session_start = turn_time
|
||||
elif (turn_time - current_session_start).total_seconds() > 3600: # 1 hour gap = new session
|
||||
session_starts.append(current_session_start)
|
||||
current_session_start = turn_time
|
||||
|
||||
if current_session_start:
|
||||
session_starts.append(current_session_start)
|
||||
|
||||
# Most used tools
|
||||
most_used_tools = sorted(
|
||||
self.analytics_data["tools_used_count"].items(),
|
||||
key=lambda x: x[1],
|
||||
reverse=True
|
||||
)[:5]
|
||||
|
||||
# Recent activity pattern
|
||||
recent_activity = {}
|
||||
for turn in self.conversation_history[-20:]: # Last 20 turns
|
||||
date = turn.timestamp.split('T')[0] # Get date part
|
||||
if date in recent_activity:
|
||||
recent_activity[date] += 1
|
||||
else:
|
||||
recent_activity[date] = 1
|
||||
|
||||
return {
|
||||
**self.analytics_data,
|
||||
"most_used_tools": most_used_tools,
|
||||
"recent_activity_pattern": recent_activity,
|
||||
"active_workflows_count": len(self.get_active_workflows()),
|
||||
"paused_workflows_count": len(self.get_paused_workflows()),
|
||||
"conversation_turns": len(self.conversation_history)
|
||||
}
|
||||
|
||||
def export_conversation_history(self, format: str = "json") -> str:
|
||||
"""Export conversation history in specified format."""
|
||||
if format.lower() == "json":
|
||||
return json.dumps([asdict(turn) for turn in self.conversation_history], indent=2)
|
||||
elif format.lower() == "txt":
|
||||
text_export = []
|
||||
for turn in self.conversation_history:
|
||||
text_export.append(f"[{turn.timestamp}] User: {turn.user_input}")
|
||||
text_export.append(f"Intent: {turn.intent}, Tools: {', '.join(turn.tools_used)}")
|
||||
text_export.append(f"Response: {turn.response_summary}")
|
||||
text_export.append("-" * 50)
|
||||
return "\n".join(text_export)
|
||||
else:
|
||||
raise ValueError("Unsupported export format. Use 'json' or 'txt'.")
|
||||
|
||||
def cleanup_old_data(self, days: int = 30):
|
||||
"""Clean up old conversation data beyond specified days."""
|
||||
cutoff_date = datetime.now() - timedelta(days=days)
|
||||
|
||||
self.conversation_history = [
|
||||
turn for turn in self.conversation_history
|
||||
if datetime.fromisoformat(turn.timestamp) > cutoff_date
|
||||
]
|
||||
|
||||
self.save_context()
|
||||
|
||||
def save_context(self):
|
||||
"""Save context data to file."""
|
||||
try:
|
||||
context_data = {
|
||||
"user_id": self.user_id,
|
||||
"conversation_history": [asdict(turn) for turn in self.conversation_history],
|
||||
"user_preferences": asdict(self.user_preferences),
|
||||
"active_workflows": [asdict(workflow) for workflow in self.active_workflows],
|
||||
"analytics_data": self.analytics_data,
|
||||
"last_saved": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
with open(self.context_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(context_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error saving context: {e}")
|
||||
|
||||
def load_context(self):
|
||||
"""Load context data from file."""
|
||||
try:
|
||||
if os.path.exists(self.context_path):
|
||||
with open(self.context_path, 'r', encoding='utf-8') as f:
|
||||
context_data = json.load(f)
|
||||
|
||||
# Load conversation history
|
||||
self.conversation_history = [
|
||||
ConversationTurn(**turn_data)
|
||||
for turn_data in context_data.get("conversation_history", [])
|
||||
]
|
||||
|
||||
# Load user preferences
|
||||
prefs_data = context_data.get("user_preferences", {})
|
||||
if prefs_data:
|
||||
self.user_preferences = UserPreferences(**prefs_data)
|
||||
|
||||
# Load active workflows
|
||||
self.active_workflows = [
|
||||
WorkflowState(**workflow_data)
|
||||
for workflow_data in context_data.get("active_workflows", [])
|
||||
]
|
||||
|
||||
# Load analytics data
|
||||
self.analytics_data.update(context_data.get("analytics_data", {}))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading context: {e}")
|
||||
# Continue with default values if loading fails
|
||||
|
||||
def reset_context(self):
|
||||
"""Reset all context data (use with caution)."""
|
||||
self.conversation_history.clear()
|
||||
self.active_workflows.clear()
|
||||
self.session_data.clear()
|
||||
self.analytics_data = {
|
||||
"total_interactions": 0,
|
||||
"tools_used_count": {},
|
||||
"workflows_completed": 0,
|
||||
"average_session_length": 0,
|
||||
"last_active": None
|
||||
}
|
||||
|
||||
# Reset user preferences to defaults
|
||||
self.user_preferences = UserPreferences(
|
||||
content_preferences=[],
|
||||
preferred_tone="professional",
|
||||
preferred_length="medium",
|
||||
industry_focus=[],
|
||||
language="en",
|
||||
timezone="UTC",
|
||||
notification_settings={}
|
||||
)
|
||||
|
||||
self.save_context()
|
||||
|
||||
def get_context_summary(self) -> str:
|
||||
"""Get a human-readable summary of the current context."""
|
||||
summary_parts = []
|
||||
|
||||
# Basic stats
|
||||
summary_parts.append(f"Total interactions: {self.analytics_data['total_interactions']}")
|
||||
summary_parts.append(f"Conversation turns: {len(self.conversation_history)}")
|
||||
|
||||
# Active workflows
|
||||
active_workflows = self.get_active_workflows()
|
||||
if active_workflows:
|
||||
workflow_names = [w.workflow_name for w in active_workflows]
|
||||
summary_parts.append(f"Active workflows: {', '.join(workflow_names)}")
|
||||
|
||||
# Recent topics
|
||||
recent_topics = self.get_recent_topics(hours=6) # Last 6 hours
|
||||
if recent_topics:
|
||||
summary_parts.append(f"Recent topics: {', '.join(recent_topics[:5])}")
|
||||
|
||||
# User preferences
|
||||
if self.user_preferences.content_preferences:
|
||||
summary_parts.append(f"Content preferences: {', '.join(self.user_preferences.content_preferences)}")
|
||||
|
||||
summary_parts.append(f"Preferred tone: {self.user_preferences.preferred_tone}")
|
||||
|
||||
return "\n".join(summary_parts)
|
||||
@@ -1,413 +0,0 @@
|
||||
"""
|
||||
Intent Analyzer for Enhanced ALwrity Chatbot.
|
||||
|
||||
Advanced user intent analysis with context awareness and multi-intent detection.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any
|
||||
|
||||
|
||||
class IntentAnalyzer:
|
||||
"""Advanced user intent analysis with context awareness."""
|
||||
|
||||
def __init__(self):
|
||||
self.intent_keywords = {
|
||||
"write": {
|
||||
"keywords": ["write", "create", "generate", "compose", "draft", "author", "produce", "craft"],
|
||||
"sub_intents": ["blog", "article", "story", "social", "product", "email", "copy", "script"]
|
||||
},
|
||||
"analyze": {
|
||||
"keywords": ["analyze", "review", "check", "examine", "evaluate", "audit", "assess", "study"],
|
||||
"sub_intents": ["seo", "competitor", "website", "content", "performance", "traffic", "keywords"]
|
||||
},
|
||||
"seo": {
|
||||
"keywords": ["seo", "optimize", "rank", "keyword", "search", "meta", "visibility", "serp"],
|
||||
"sub_intents": ["on_page", "technical", "content_gap", "backlinks", "local", "mobile"]
|
||||
},
|
||||
"social": {
|
||||
"keywords": ["social", "facebook", "twitter", "linkedin", "instagram", "youtube", "tiktok"],
|
||||
"sub_intents": ["post", "campaign", "engagement", "hashtags", "stories", "ads"]
|
||||
},
|
||||
"research": {
|
||||
"keywords": ["research", "competitor", "market", "trend", "keyword", "analysis", "study"],
|
||||
"sub_intents": ["competitor", "keyword", "market", "content_gap", "audience", "trends"]
|
||||
},
|
||||
"plan": {
|
||||
"keywords": ["plan", "strategy", "calendar", "schedule", "roadmap", "organize", "structure"],
|
||||
"sub_intents": ["content_calendar", "strategy", "campaign", "workflow", "editorial"]
|
||||
},
|
||||
"workflow": {
|
||||
"keywords": ["workflow", "automate", "process", "step", "guide", "complete", "pipeline"],
|
||||
"sub_intents": ["blog_creation", "seo_audit", "social_campaign", "content_strategy"]
|
||||
},
|
||||
"optimize": {
|
||||
"keywords": ["optimize", "improve", "enhance", "boost", "increase", "maximize", "refine"],
|
||||
"sub_intents": ["seo", "content", "performance", "conversion", "speed", "engagement"]
|
||||
},
|
||||
"learn": {
|
||||
"keywords": ["learn", "how", "tutorial", "guide", "help", "explain", "teach", "show"],
|
||||
"sub_intents": ["seo", "content", "social", "tools", "strategy", "best_practices"]
|
||||
},
|
||||
"fix": {
|
||||
"keywords": ["fix", "solve", "repair", "troubleshoot", "debug", "resolve", "correct"],
|
||||
"sub_intents": ["seo_issues", "technical", "content", "performance", "errors"]
|
||||
}
|
||||
}
|
||||
|
||||
self.content_type_keywords = {
|
||||
"blog": ["blog", "article", "post", "content"],
|
||||
"social": ["social", "post", "tweet", "update", "story"],
|
||||
"email": ["email", "newsletter", "campaign", "sequence"],
|
||||
"video": ["video", "youtube", "script", "transcript"],
|
||||
"ad": ["ad", "advertisement", "promotion", "campaign"],
|
||||
"product": ["product", "description", "listing", "catalog"],
|
||||
"news": ["news", "press", "announcement", "release"],
|
||||
"story": ["story", "narrative", "fiction", "creative"],
|
||||
"technical": ["technical", "documentation", "manual", "guide"],
|
||||
"academic": ["academic", "research", "paper", "thesis"]
|
||||
}
|
||||
|
||||
self.urgency_keywords = {
|
||||
"high": ["urgent", "asap", "immediately", "emergency", "critical", "now"],
|
||||
"medium": ["soon", "quickly", "fast", "priority", "important"],
|
||||
"low": ["eventually", "when possible", "later", "sometime"]
|
||||
}
|
||||
|
||||
self.complexity_indicators = {
|
||||
"high": ["comprehensive", "detailed", "complete", "full", "extensive", "thorough"],
|
||||
"medium": ["moderate", "standard", "regular", "normal", "typical"],
|
||||
"low": ["simple", "basic", "quick", "brief", "short", "minimal"]
|
||||
}
|
||||
|
||||
def analyze_user_intent(self, prompt: str, context: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""Enhanced user intent analysis with context awareness."""
|
||||
prompt_lower = prompt.lower()
|
||||
|
||||
# Detect primary and secondary intents
|
||||
detected_intents = self._detect_intents(prompt_lower)
|
||||
|
||||
# Detect sub-intents
|
||||
sub_intents = self._detect_sub_intents(prompt_lower, detected_intents)
|
||||
|
||||
# Determine content types
|
||||
content_types = self._detect_content_types(prompt_lower)
|
||||
|
||||
# Assess urgency
|
||||
urgency = self._assess_urgency(prompt_lower)
|
||||
|
||||
# Determine complexity
|
||||
complexity = self._assess_complexity(prompt_lower)
|
||||
|
||||
# Calculate confidence scores
|
||||
confidence_scores = self._calculate_confidence_scores(prompt_lower, detected_intents)
|
||||
|
||||
# Context-aware enhancements
|
||||
if context:
|
||||
detected_intents, confidence_scores = self._enhance_with_context(
|
||||
detected_intents, confidence_scores, context, prompt_lower
|
||||
)
|
||||
|
||||
# Determine primary intent
|
||||
primary_intent = self._determine_primary_intent(detected_intents, confidence_scores)
|
||||
|
||||
# Generate suggestions
|
||||
suggested_workflows = self._suggest_workflows(detected_intents, content_types)
|
||||
suggested_tools = self._suggest_tools(detected_intents, sub_intents, content_types)
|
||||
|
||||
return {
|
||||
"primary_intent": primary_intent,
|
||||
"all_intents": detected_intents,
|
||||
"sub_intents": sub_intents,
|
||||
"content_types": content_types,
|
||||
"confidence_scores": confidence_scores,
|
||||
"urgency": urgency,
|
||||
"complexity": complexity,
|
||||
"suggested_workflows": suggested_workflows,
|
||||
"suggested_tools": suggested_tools,
|
||||
"intent_strength": self._calculate_intent_strength(confidence_scores),
|
||||
"multi_intent": len(detected_intents) > 1,
|
||||
"context_enhanced": context is not None
|
||||
}
|
||||
|
||||
def _detect_intents(self, prompt_lower: str) -> List[str]:
|
||||
"""Detect all intents in the user prompt."""
|
||||
detected_intents = []
|
||||
|
||||
for intent, data in self.intent_keywords.items():
|
||||
matches = sum(1 for keyword in data["keywords"] if keyword in prompt_lower)
|
||||
if matches > 0:
|
||||
detected_intents.append(intent)
|
||||
|
||||
return detected_intents
|
||||
|
||||
def _detect_sub_intents(self, prompt_lower: str, detected_intents: List[str]) -> List[str]:
|
||||
"""Detect sub-intents based on primary intents."""
|
||||
sub_intents = []
|
||||
|
||||
for intent in detected_intents:
|
||||
if intent in self.intent_keywords:
|
||||
for sub_intent in self.intent_keywords[intent]["sub_intents"]:
|
||||
if sub_intent in prompt_lower:
|
||||
sub_intents.append(sub_intent)
|
||||
|
||||
return list(set(sub_intents)) # Remove duplicates
|
||||
|
||||
def _detect_content_types(self, prompt_lower: str) -> List[str]:
|
||||
"""Detect content types mentioned in the prompt."""
|
||||
content_types = []
|
||||
|
||||
for content_type, keywords in self.content_type_keywords.items():
|
||||
if any(keyword in prompt_lower for keyword in keywords):
|
||||
content_types.append(content_type)
|
||||
|
||||
return content_types
|
||||
|
||||
def _assess_urgency(self, prompt_lower: str) -> Dict[str, Any]:
|
||||
"""Assess the urgency level of the request."""
|
||||
urgency_level = "normal"
|
||||
urgency_score = 0.5
|
||||
|
||||
for level, keywords in self.urgency_keywords.items():
|
||||
matches = sum(1 for keyword in keywords if keyword in prompt_lower)
|
||||
if matches > 0:
|
||||
if level == "high":
|
||||
urgency_level = "high"
|
||||
urgency_score = 0.9
|
||||
break
|
||||
elif level == "medium" and urgency_level == "normal":
|
||||
urgency_level = "medium"
|
||||
urgency_score = 0.7
|
||||
elif level == "low" and urgency_level == "normal":
|
||||
urgency_level = "low"
|
||||
urgency_score = 0.3
|
||||
|
||||
return {
|
||||
"level": urgency_level,
|
||||
"score": urgency_score,
|
||||
"is_urgent": urgency_level in ["high", "medium"]
|
||||
}
|
||||
|
||||
def _assess_complexity(self, prompt_lower: str) -> Dict[str, Any]:
|
||||
"""Assess the complexity level of the request."""
|
||||
complexity_level = "medium"
|
||||
complexity_score = 0.5
|
||||
|
||||
for level, keywords in self.complexity_indicators.items():
|
||||
matches = sum(1 for keyword in keywords if keyword in prompt_lower)
|
||||
if matches > 0:
|
||||
complexity_level = level
|
||||
complexity_score = {"high": 0.9, "medium": 0.5, "low": 0.3}[level]
|
||||
break
|
||||
|
||||
# Additional complexity indicators
|
||||
word_count = len(prompt_lower.split())
|
||||
if word_count > 50:
|
||||
complexity_score = min(complexity_score + 0.2, 1.0)
|
||||
elif word_count < 10:
|
||||
complexity_score = max(complexity_score - 0.2, 0.1)
|
||||
|
||||
return {
|
||||
"level": complexity_level,
|
||||
"score": complexity_score,
|
||||
"word_count": word_count
|
||||
}
|
||||
|
||||
def _calculate_confidence_scores(self, prompt_lower: str, detected_intents: List[str]) -> Dict[str, float]:
|
||||
"""Calculate confidence scores for detected intents."""
|
||||
confidence_scores = {}
|
||||
|
||||
for intent in detected_intents:
|
||||
if intent in self.intent_keywords:
|
||||
keywords = self.intent_keywords[intent]["keywords"]
|
||||
matches = sum(1 for keyword in keywords if keyword in prompt_lower)
|
||||
confidence = matches / len(keywords)
|
||||
|
||||
# Boost confidence for exact matches
|
||||
if intent in prompt_lower:
|
||||
confidence += 0.3
|
||||
|
||||
# Boost confidence for multiple keyword matches
|
||||
if matches > 2:
|
||||
confidence += 0.2
|
||||
|
||||
confidence_scores[intent] = min(confidence, 1.0)
|
||||
|
||||
return confidence_scores
|
||||
|
||||
def _enhance_with_context(self, detected_intents: List[str], confidence_scores: Dict[str, float],
|
||||
context: Dict[str, Any], prompt_lower: str) -> tuple:
|
||||
"""Enhance intent detection with conversation context."""
|
||||
enhanced_intents = detected_intents.copy()
|
||||
enhanced_scores = confidence_scores.copy()
|
||||
|
||||
# Recent conversation topics
|
||||
recent_topics = context.get("recent_topics", [])
|
||||
for topic in recent_topics:
|
||||
if topic.lower() in prompt_lower:
|
||||
# Boost related intents
|
||||
for intent in self.intent_keywords:
|
||||
if topic.lower() in self.intent_keywords[intent]["keywords"]:
|
||||
if intent in enhanced_scores:
|
||||
enhanced_scores[intent] += 0.1
|
||||
else:
|
||||
enhanced_intents.append(intent)
|
||||
enhanced_scores[intent] = 0.4
|
||||
|
||||
# User preferences
|
||||
user_prefs = context.get("user_preferences", {})
|
||||
if user_prefs.get("content_preferences"):
|
||||
for pref in user_prefs["content_preferences"]:
|
||||
if pref in prompt_lower:
|
||||
# Boost content creation intents
|
||||
if "write" in enhanced_scores:
|
||||
enhanced_scores["write"] += 0.15
|
||||
|
||||
# Active workflows
|
||||
active_workflows = context.get("active_workflows", [])
|
||||
if active_workflows:
|
||||
# Boost workflow-related intents
|
||||
if "workflow" in enhanced_scores:
|
||||
enhanced_scores["workflow"] += 0.2
|
||||
else:
|
||||
enhanced_intents.append("workflow")
|
||||
enhanced_scores["workflow"] = 0.6
|
||||
|
||||
# Tool usage history
|
||||
tool_history = context.get("tool_usage_history", [])
|
||||
if tool_history:
|
||||
last_tools = tool_history[-3:] # Last 3 tools
|
||||
for tool in last_tools:
|
||||
# Map tools to intents and boost related intents
|
||||
tool_intent_mapping = {
|
||||
"ai_blog_writer": "write",
|
||||
"content_gap_analysis": "analyze",
|
||||
"technical_seo": "seo",
|
||||
"linkedin_writer": "social"
|
||||
}
|
||||
|
||||
if tool in tool_intent_mapping:
|
||||
intent = tool_intent_mapping[tool]
|
||||
if intent in enhanced_scores:
|
||||
enhanced_scores[intent] += 0.1
|
||||
|
||||
return enhanced_intents, enhanced_scores
|
||||
|
||||
def _determine_primary_intent(self, detected_intents: List[str], confidence_scores: Dict[str, float]) -> str:
|
||||
"""Determine the primary intent from detected intents."""
|
||||
if not detected_intents:
|
||||
return "general"
|
||||
|
||||
if len(detected_intents) == 1:
|
||||
return detected_intents[0]
|
||||
|
||||
# Return intent with highest confidence
|
||||
primary_intent = max(detected_intents, key=lambda x: confidence_scores.get(x, 0))
|
||||
return primary_intent
|
||||
|
||||
def _suggest_workflows(self, detected_intents: List[str], content_types: List[str]) -> List[str]:
|
||||
"""Suggest relevant workflows based on intents and content types."""
|
||||
suggested_workflows = []
|
||||
|
||||
# Intent-based workflow suggestions
|
||||
workflow_mapping = {
|
||||
"write": ["blog_creation_workflow", "content_strategy_workflow"],
|
||||
"analyze": ["competitor_analysis_workflow", "seo_audit_workflow"],
|
||||
"seo": ["seo_audit_workflow", "content_gap_workflow"],
|
||||
"social": ["social_media_workflow", "content_repurposing_workflow"],
|
||||
"plan": ["content_strategy_workflow", "editorial_calendar_workflow"]
|
||||
}
|
||||
|
||||
for intent in detected_intents:
|
||||
if intent in workflow_mapping:
|
||||
suggested_workflows.extend(workflow_mapping[intent])
|
||||
|
||||
# Content type specific workflows
|
||||
if "blog" in content_types:
|
||||
suggested_workflows.append("blog_creation_workflow")
|
||||
if "social" in content_types:
|
||||
suggested_workflows.append("social_media_workflow")
|
||||
|
||||
return list(set(suggested_workflows)) # Remove duplicates
|
||||
|
||||
def _suggest_tools(self, detected_intents: List[str], sub_intents: List[str],
|
||||
content_types: List[str]) -> List[str]:
|
||||
"""Suggest relevant tools based on intents, sub-intents, and content types."""
|
||||
suggested_tools = []
|
||||
|
||||
# Intent-based tool suggestions
|
||||
tool_mapping = {
|
||||
"write": ["ai_blog_writer", "story_writer", "email_writer"],
|
||||
"analyze": ["content_gap_analysis", "website_analyzer", "competitor_analyzer"],
|
||||
"seo": ["technical_seo", "on_page_seo", "keyword_research"],
|
||||
"social": ["linkedin_writer", "facebook_writer", "social_campaign"],
|
||||
"research": ["competitor_analysis", "keyword_research", "market_research"],
|
||||
"optimize": ["seo_optimizer", "content_optimizer", "performance_optimizer"]
|
||||
}
|
||||
|
||||
for intent in detected_intents:
|
||||
if intent in tool_mapping:
|
||||
suggested_tools.extend(tool_mapping[intent])
|
||||
|
||||
# Sub-intent specific tools
|
||||
sub_intent_tools = {
|
||||
"blog": ["ai_blog_writer", "seo_optimizer"],
|
||||
"competitor": ["competitor_analysis", "content_gap_analysis"],
|
||||
"technical": ["technical_seo", "performance_analyzer"],
|
||||
"social": ["linkedin_writer", "facebook_writer"]
|
||||
}
|
||||
|
||||
for sub_intent in sub_intents:
|
||||
if sub_intent in sub_intent_tools:
|
||||
suggested_tools.extend(sub_intent_tools[sub_intent])
|
||||
|
||||
# Content type specific tools
|
||||
content_tools = {
|
||||
"blog": ["ai_blog_writer", "seo_optimizer"],
|
||||
"social": ["linkedin_writer", "facebook_writer"],
|
||||
"email": ["email_writer", "campaign_creator"],
|
||||
"video": ["youtube_writer", "script_generator"]
|
||||
}
|
||||
|
||||
for content_type in content_types:
|
||||
if content_type in content_tools:
|
||||
suggested_tools.extend(content_tools[content_type])
|
||||
|
||||
return list(set(suggested_tools)) # Remove duplicates
|
||||
|
||||
def _calculate_intent_strength(self, confidence_scores: Dict[str, float]) -> str:
|
||||
"""Calculate overall intent strength."""
|
||||
if not confidence_scores:
|
||||
return "weak"
|
||||
|
||||
max_confidence = max(confidence_scores.values())
|
||||
avg_confidence = sum(confidence_scores.values()) / len(confidence_scores)
|
||||
|
||||
if max_confidence >= 0.8 and avg_confidence >= 0.6:
|
||||
return "strong"
|
||||
elif max_confidence >= 0.6 or avg_confidence >= 0.4:
|
||||
return "moderate"
|
||||
else:
|
||||
return "weak"
|
||||
|
||||
def get_intent_explanation(self, intent_analysis: Dict[str, Any]) -> str:
|
||||
"""Generate a human-readable explanation of the intent analysis."""
|
||||
primary = intent_analysis["primary_intent"]
|
||||
confidence = intent_analysis["confidence_scores"].get(primary, 0)
|
||||
urgency = intent_analysis["urgency"]["level"]
|
||||
complexity = intent_analysis["complexity"]["level"]
|
||||
|
||||
explanation = f"Primary intent: {primary} (confidence: {confidence:.2f})\n"
|
||||
|
||||
if intent_analysis["multi_intent"]:
|
||||
other_intents = [i for i in intent_analysis["all_intents"] if i != primary]
|
||||
explanation += f"Additional intents: {', '.join(other_intents)}\n"
|
||||
|
||||
if intent_analysis["content_types"]:
|
||||
explanation += f"Content types: {', '.join(intent_analysis['content_types'])}\n"
|
||||
|
||||
explanation += f"Urgency: {urgency}, Complexity: {complexity}\n"
|
||||
|
||||
if intent_analysis["suggested_tools"]:
|
||||
explanation += f"Recommended tools: {', '.join(intent_analysis['suggested_tools'][:3])}"
|
||||
|
||||
return explanation
|
||||
@@ -1,285 +0,0 @@
|
||||
"""
|
||||
Smart Tool Router for Enhanced ALwrity Chatbot.
|
||||
|
||||
Intelligent tool routing based on user intent and context.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any
|
||||
|
||||
|
||||
class SmartToolRouter:
|
||||
"""Intelligent tool routing based on user intent and context."""
|
||||
|
||||
def __init__(self):
|
||||
self.tool_categories = {
|
||||
"content_creation": [
|
||||
"ai_blog_writer", "story_writer", "essay_writer",
|
||||
"product_description", "email_writer", "news_writer"
|
||||
],
|
||||
"seo_tools": [
|
||||
"content_gap_analysis", "technical_seo", "on_page_seo",
|
||||
"competitor_analysis", "keyword_research", "meta_generator"
|
||||
],
|
||||
"social_media": [
|
||||
"linkedin_writer", "facebook_writer", "youtube_writer",
|
||||
"instagram_writer", "twitter_writer", "social_campaign"
|
||||
],
|
||||
"analysis": [
|
||||
"website_analyzer", "content_analyzer", "competitor_analyzer",
|
||||
"performance_analyzer", "seo_analyzer"
|
||||
],
|
||||
"planning": [
|
||||
"content_calendar", "content_repurposing", "strategy_planner",
|
||||
"campaign_planner", "editorial_calendar"
|
||||
],
|
||||
"optimization": [
|
||||
"seo_optimizer", "content_optimizer", "performance_optimizer",
|
||||
"conversion_optimizer", "speed_optimizer"
|
||||
]
|
||||
}
|
||||
|
||||
self.intent_tool_mapping = {
|
||||
"write": ["ai_blog_writer", "story_writer", "essay_writer", "email_writer"],
|
||||
"analyze": ["content_gap_analysis", "technical_seo", "website_analyzer", "competitor_analyzer"],
|
||||
"seo": ["on_page_seo", "technical_seo", "content_gap_analysis", "seo_optimizer"],
|
||||
"social": ["linkedin_writer", "facebook_writer", "youtube_writer", "social_campaign"],
|
||||
"plan": ["content_calendar", "content_repurposing", "strategy_planner", "campaign_planner"],
|
||||
"research": ["competitor_analysis", "content_gap_analysis", "keyword_research", "market_research"],
|
||||
"optimize": ["seo_optimizer", "content_optimizer", "performance_optimizer"],
|
||||
"create": ["ai_blog_writer", "content_creator", "social_content_creation"],
|
||||
"audit": ["technical_seo", "seo_analyzer", "website_analyzer", "performance_analyzer"]
|
||||
}
|
||||
|
||||
# Tool confidence weights based on effectiveness
|
||||
self.tool_weights = {
|
||||
"ai_blog_writer": 0.9,
|
||||
"content_gap_analysis": 0.85,
|
||||
"technical_seo": 0.8,
|
||||
"linkedin_writer": 0.85,
|
||||
"competitor_analysis": 0.8,
|
||||
"seo_optimizer": 0.75,
|
||||
"content_calendar": 0.7
|
||||
}
|
||||
|
||||
def route_to_tools(self, user_intent: str, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Route user intent to relevant tools with confidence scoring."""
|
||||
suggested_tools = []
|
||||
user_intent_lower = user_intent.lower()
|
||||
|
||||
# Primary intent matching
|
||||
for intent, tools in self.intent_tool_mapping.items():
|
||||
if intent in user_intent_lower:
|
||||
for tool in tools:
|
||||
confidence = self._calculate_confidence(intent, user_intent, context)
|
||||
suggested_tools.append({
|
||||
"tool": tool,
|
||||
"category": self._get_tool_category(tool),
|
||||
"confidence": confidence,
|
||||
"intent_match": intent,
|
||||
"reason": f"Matches '{intent}' intent"
|
||||
})
|
||||
|
||||
# Context-based suggestions
|
||||
context_tools = self._get_context_based_suggestions(context, user_intent)
|
||||
suggested_tools.extend(context_tools)
|
||||
|
||||
# Remove duplicates and sort by confidence
|
||||
unique_tools = {}
|
||||
for tool in suggested_tools:
|
||||
tool_name = tool["tool"]
|
||||
if tool_name not in unique_tools or tool["confidence"] > unique_tools[tool_name]["confidence"]:
|
||||
unique_tools[tool_name] = tool
|
||||
|
||||
# Sort by confidence and return top suggestions
|
||||
sorted_tools = sorted(unique_tools.values(), key=lambda x: x["confidence"], reverse=True)
|
||||
return sorted_tools[:8] # Return top 8 suggestions
|
||||
|
||||
def _get_tool_category(self, tool: str) -> str:
|
||||
"""Get category for a tool."""
|
||||
for category, tools in self.tool_categories.items():
|
||||
if tool in tools:
|
||||
return category
|
||||
return "general"
|
||||
|
||||
def _calculate_confidence(self, intent: str, user_text: str, context: Dict[str, Any]) -> float:
|
||||
"""Calculate confidence score for tool suggestion."""
|
||||
base_score = 0.5
|
||||
user_text_lower = user_text.lower()
|
||||
|
||||
# Intent match bonus
|
||||
if intent in user_text_lower:
|
||||
base_score += 0.3
|
||||
|
||||
# Keyword bonuses
|
||||
keyword_bonuses = {
|
||||
"write": ["create", "generate", "compose", "draft", "author", "produce"],
|
||||
"analyze": ["check", "review", "examine", "evaluate", "assess", "study"],
|
||||
"seo": ["optimize", "rank", "search", "keywords", "meta", "visibility"],
|
||||
"social": ["post", "share", "engage", "campaign", "viral", "audience"],
|
||||
"plan": ["schedule", "organize", "strategy", "roadmap", "timeline"],
|
||||
"research": ["study", "investigate", "explore", "discover", "find"]
|
||||
}
|
||||
|
||||
if intent in keyword_bonuses:
|
||||
for keyword in keyword_bonuses[intent]:
|
||||
if keyword in user_text_lower:
|
||||
base_score += 0.1
|
||||
|
||||
# Context bonuses
|
||||
if context:
|
||||
# Recent tool usage
|
||||
recent_tools = context.get('tool_usage_history', [])[-3:]
|
||||
if any(tool in user_text_lower for tool in recent_tools):
|
||||
base_score += 0.15
|
||||
|
||||
# User preferences
|
||||
user_prefs = context.get('user_preferences', {})
|
||||
if user_prefs.get('industry') and user_prefs['industry'].lower() in user_text_lower:
|
||||
base_score += 0.1
|
||||
|
||||
# Urgency bonus
|
||||
urgency_keywords = ["urgent", "asap", "quickly", "fast", "immediate", "now"]
|
||||
if any(keyword in user_text_lower for keyword in urgency_keywords):
|
||||
base_score += 0.1
|
||||
|
||||
return min(base_score, 1.0)
|
||||
|
||||
def _get_context_based_suggestions(self, context: Dict[str, Any], user_intent: str) -> List[Dict[str, Any]]:
|
||||
"""Get tool suggestions based on conversation context."""
|
||||
context_tools = []
|
||||
|
||||
if not context:
|
||||
return context_tools
|
||||
|
||||
# Recent tool usage patterns
|
||||
recent_tools = context.get('tool_usage_history', [])
|
||||
if recent_tools:
|
||||
# Suggest complementary tools
|
||||
last_tool = recent_tools[-1] if recent_tools else None
|
||||
complementary_tools = self._get_complementary_tools(last_tool)
|
||||
|
||||
for tool in complementary_tools:
|
||||
context_tools.append({
|
||||
"tool": tool,
|
||||
"category": self._get_tool_category(tool),
|
||||
"confidence": 0.6,
|
||||
"intent_match": "context",
|
||||
"reason": f"Complements recent use of {last_tool}"
|
||||
})
|
||||
|
||||
# Active workflows
|
||||
active_workflows = context.get('active_workflows', [])
|
||||
if active_workflows:
|
||||
# Suggest tools for current workflow steps
|
||||
for workflow in active_workflows:
|
||||
workflow_tools = self._get_workflow_tools(workflow)
|
||||
for tool in workflow_tools:
|
||||
context_tools.append({
|
||||
"tool": tool,
|
||||
"category": self._get_tool_category(tool),
|
||||
"confidence": 0.7,
|
||||
"intent_match": "workflow",
|
||||
"reason": f"Next step in {workflow} workflow"
|
||||
})
|
||||
|
||||
# User preferences
|
||||
user_prefs = context.get('user_preferences', {})
|
||||
if user_prefs.get('content_preferences'):
|
||||
pref_tools = self._get_preference_based_tools(user_prefs['content_preferences'])
|
||||
for tool in pref_tools:
|
||||
context_tools.append({
|
||||
"tool": tool,
|
||||
"category": self._get_tool_category(tool),
|
||||
"confidence": 0.65,
|
||||
"intent_match": "preference",
|
||||
"reason": "Based on your content preferences"
|
||||
})
|
||||
|
||||
return context_tools
|
||||
|
||||
def _get_complementary_tools(self, last_tool: str) -> List[str]:
|
||||
"""Get tools that complement the last used tool."""
|
||||
complementary_mapping = {
|
||||
"ai_blog_writer": ["seo_optimizer", "meta_generator", "content_gap_analysis"],
|
||||
"content_gap_analysis": ["ai_blog_writer", "keyword_research", "competitor_analysis"],
|
||||
"technical_seo": ["on_page_seo", "content_optimizer", "performance_analyzer"],
|
||||
"linkedin_writer": ["social_campaign", "content_calendar", "hashtag_research"],
|
||||
"competitor_analysis": ["content_gap_analysis", "keyword_research", "strategy_planner"],
|
||||
"keyword_research": ["ai_blog_writer", "content_gap_analysis", "seo_optimizer"]
|
||||
}
|
||||
|
||||
return complementary_mapping.get(last_tool, [])
|
||||
|
||||
def _get_workflow_tools(self, workflow: str) -> List[str]:
|
||||
"""Get tools associated with a specific workflow."""
|
||||
workflow_tools = {
|
||||
"blog_creation_workflow": ["keyword_research", "ai_blog_writer", "seo_optimizer"],
|
||||
"competitor_analysis_workflow": ["competitor_analysis", "content_gap_analysis"],
|
||||
"social_media_workflow": ["linkedin_writer", "facebook_writer", "social_campaign"],
|
||||
"seo_audit_workflow": ["technical_seo", "on_page_seo", "competitor_analysis"]
|
||||
}
|
||||
|
||||
return workflow_tools.get(workflow, [])
|
||||
|
||||
def _get_preference_based_tools(self, content_preferences: List[str]) -> List[str]:
|
||||
"""Get tools based on user content preferences."""
|
||||
preference_tools = []
|
||||
|
||||
for pref in content_preferences:
|
||||
if pref in ["blog", "article"]:
|
||||
preference_tools.extend(["ai_blog_writer", "seo_optimizer"])
|
||||
elif pref in ["social", "post"]:
|
||||
preference_tools.extend(["linkedin_writer", "facebook_writer"])
|
||||
elif pref in ["seo", "optimization"]:
|
||||
preference_tools.extend(["technical_seo", "on_page_seo"])
|
||||
|
||||
return list(set(preference_tools)) # Remove duplicates
|
||||
|
||||
def get_tool_info(self, tool_name: str) -> Dict[str, Any]:
|
||||
"""Get detailed information about a specific tool."""
|
||||
tool_info = {
|
||||
"ai_blog_writer": {
|
||||
"name": "AI Blog Writer",
|
||||
"description": "Create comprehensive, SEO-optimized blog posts",
|
||||
"category": "content_creation",
|
||||
"use_cases": ["Blog posts", "Articles", "Long-form content"],
|
||||
"estimated_time": "5-10 minutes"
|
||||
},
|
||||
"content_gap_analysis": {
|
||||
"name": "Content Gap Analysis",
|
||||
"description": "Identify content opportunities vs competitors",
|
||||
"category": "seo_tools",
|
||||
"use_cases": ["Competitor research", "Content strategy", "SEO planning"],
|
||||
"estimated_time": "10-15 minutes"
|
||||
},
|
||||
"technical_seo": {
|
||||
"name": "Technical SEO Crawler",
|
||||
"description": "Comprehensive technical SEO audit",
|
||||
"category": "seo_tools",
|
||||
"use_cases": ["Site audits", "Technical issues", "Performance analysis"],
|
||||
"estimated_time": "15-20 minutes"
|
||||
},
|
||||
"linkedin_writer": {
|
||||
"name": "LinkedIn Writer",
|
||||
"description": "Create professional LinkedIn content",
|
||||
"category": "social_media",
|
||||
"use_cases": ["LinkedIn posts", "Professional articles", "Networking content"],
|
||||
"estimated_time": "3-5 minutes"
|
||||
}
|
||||
}
|
||||
|
||||
return tool_info.get(tool_name, {
|
||||
"name": tool_name.replace('_', ' ').title(),
|
||||
"description": f"ALwrity {tool_name.replace('_', ' ')} tool",
|
||||
"category": self._get_tool_category(tool_name),
|
||||
"use_cases": ["Content creation", "Analysis", "Optimization"],
|
||||
"estimated_time": "5-10 minutes"
|
||||
})
|
||||
|
||||
def get_category_tools(self, category: str) -> List[str]:
|
||||
"""Get all tools in a specific category."""
|
||||
return self.tool_categories.get(category, [])
|
||||
|
||||
def get_all_categories(self) -> List[str]:
|
||||
"""Get all available tool categories."""
|
||||
return list(self.tool_categories.keys())
|
||||
@@ -1,171 +0,0 @@
|
||||
"""
|
||||
Workflow Engine for Enhanced ALwrity Chatbot.
|
||||
|
||||
Handles multi-tool workflows and automation for complex content creation tasks.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any
|
||||
|
||||
|
||||
class WorkflowEngine:
|
||||
"""Handles multi-tool workflows and automation."""
|
||||
|
||||
def __init__(self):
|
||||
self.workflows = {
|
||||
"blog_creation_workflow": {
|
||||
"name": "Complete Blog Creation",
|
||||
"description": "From idea to published blog post",
|
||||
"steps": [
|
||||
{"tool": "keyword_research", "name": "Keyword Research"},
|
||||
{"tool": "content_gap_analysis", "name": "Content Gap Analysis"},
|
||||
{"tool": "blog_writing", "name": "Blog Writing"},
|
||||
{"tool": "seo_optimization", "name": "SEO Optimization"},
|
||||
{"tool": "meta_generation", "name": "Meta Tags Generation"}
|
||||
]
|
||||
},
|
||||
"competitor_analysis_workflow": {
|
||||
"name": "Competitor Content Strategy",
|
||||
"description": "Analyze competitors and create content plan",
|
||||
"steps": [
|
||||
{"tool": "competitor_analysis", "name": "Competitor Analysis"},
|
||||
{"tool": "content_gap_analysis", "name": "Content Gap Analysis"},
|
||||
{"tool": "content_calendar", "name": "Content Calendar Creation"},
|
||||
{"tool": "content_ideas", "name": "Content Ideas Generation"}
|
||||
]
|
||||
},
|
||||
"social_media_workflow": {
|
||||
"name": "Social Media Campaign",
|
||||
"description": "Create comprehensive social media content",
|
||||
"steps": [
|
||||
{"tool": "audience_analysis", "name": "Audience Analysis"},
|
||||
{"tool": "content_planning", "name": "Content Planning"},
|
||||
{"tool": "social_content_creation", "name": "Social Content Creation"},
|
||||
{"tool": "hashtag_research", "name": "Hashtag Research"}
|
||||
]
|
||||
},
|
||||
"seo_audit_workflow": {
|
||||
"name": "Complete SEO Audit",
|
||||
"description": "Comprehensive website SEO analysis and optimization",
|
||||
"steps": [
|
||||
{"tool": "technical_seo", "name": "Technical SEO Analysis"},
|
||||
{"tool": "on_page_seo", "name": "On-Page SEO Review"},
|
||||
{"tool": "content_gap_analysis", "name": "Content Gap Analysis"},
|
||||
{"tool": "competitor_seo", "name": "Competitor SEO Analysis"},
|
||||
{"tool": "optimization_plan", "name": "SEO Optimization Plan"}
|
||||
]
|
||||
},
|
||||
"content_strategy_workflow": {
|
||||
"name": "Content Strategy Development",
|
||||
"description": "Develop comprehensive content strategy from research to execution",
|
||||
"steps": [
|
||||
{"tool": "market_research", "name": "Market Research"},
|
||||
{"tool": "audience_analysis", "name": "Audience Analysis"},
|
||||
{"tool": "competitor_analysis", "name": "Competitor Analysis"},
|
||||
{"tool": "content_pillars", "name": "Content Pillars Definition"},
|
||||
{"tool": "content_calendar", "name": "Content Calendar Creation"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def suggest_workflows(self, user_intent: str) -> List[Dict[str, Any]]:
|
||||
"""Suggest relevant workflows based on user intent."""
|
||||
relevant_workflows = []
|
||||
user_intent_lower = user_intent.lower()
|
||||
|
||||
# Blog and content creation
|
||||
if any(word in user_intent_lower for word in ['blog', 'article', 'post', 'write', 'content']):
|
||||
relevant_workflows.append(self.workflows["blog_creation_workflow"])
|
||||
|
||||
# Competitor and market analysis
|
||||
if any(word in user_intent_lower for word in ['competitor', 'analysis', 'research', 'market']):
|
||||
relevant_workflows.append(self.workflows["competitor_analysis_workflow"])
|
||||
|
||||
# Social media
|
||||
if any(word in user_intent_lower for word in ['social', 'facebook', 'linkedin', 'campaign', 'instagram', 'twitter']):
|
||||
relevant_workflows.append(self.workflows["social_media_workflow"])
|
||||
|
||||
# SEO related
|
||||
if any(word in user_intent_lower for word in ['seo', 'optimize', 'rank', 'search', 'audit']):
|
||||
relevant_workflows.append(self.workflows["seo_audit_workflow"])
|
||||
|
||||
# Strategy and planning
|
||||
if any(word in user_intent_lower for word in ['strategy', 'plan', 'roadmap', 'framework']):
|
||||
relevant_workflows.append(self.workflows["content_strategy_workflow"])
|
||||
|
||||
return relevant_workflows
|
||||
|
||||
def get_workflow(self, workflow_id: str) -> Dict[str, Any]:
|
||||
"""Get a specific workflow by ID."""
|
||||
return self.workflows.get(workflow_id)
|
||||
|
||||
def get_all_workflows(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Get all available workflows."""
|
||||
return self.workflows
|
||||
|
||||
def create_custom_workflow(self, name: str, description: str, steps: List[Dict[str, str]]) -> str:
|
||||
"""Create a custom workflow."""
|
||||
workflow_id = f"custom_{name.lower().replace(' ', '_')}"
|
||||
self.workflows[workflow_id] = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"steps": steps,
|
||||
"custom": True
|
||||
}
|
||||
return workflow_id
|
||||
|
||||
def get_workflow_progress(self, workflow_id: str, completed_steps: List[str]) -> Dict[str, Any]:
|
||||
"""Get progress information for a workflow."""
|
||||
workflow = self.workflows.get(workflow_id)
|
||||
if not workflow:
|
||||
return {"error": "Workflow not found"}
|
||||
|
||||
total_steps = len(workflow["steps"])
|
||||
completed_count = len(completed_steps)
|
||||
progress_percentage = (completed_count / total_steps) * 100 if total_steps > 0 else 0
|
||||
|
||||
next_step = None
|
||||
if completed_count < total_steps:
|
||||
next_step = workflow["steps"][completed_count]
|
||||
|
||||
return {
|
||||
"workflow_name": workflow["name"],
|
||||
"total_steps": total_steps,
|
||||
"completed_steps": completed_count,
|
||||
"progress_percentage": progress_percentage,
|
||||
"next_step": next_step,
|
||||
"is_complete": completed_count >= total_steps
|
||||
}
|
||||
|
||||
def get_step_details(self, workflow_id: str, step_index: int) -> Dict[str, Any]:
|
||||
"""Get detailed information about a specific workflow step."""
|
||||
workflow = self.workflows.get(workflow_id)
|
||||
if not workflow or step_index >= len(workflow["steps"]):
|
||||
return {"error": "Workflow or step not found"}
|
||||
|
||||
step = workflow["steps"][step_index]
|
||||
|
||||
# Add detailed descriptions for each tool
|
||||
step_descriptions = {
|
||||
"keyword_research": "Research and identify target keywords for your content",
|
||||
"content_gap_analysis": "Analyze competitor content to find opportunities",
|
||||
"blog_writing": "Create high-quality, SEO-optimized blog content",
|
||||
"seo_optimization": "Optimize content for search engines",
|
||||
"meta_generation": "Generate meta titles and descriptions",
|
||||
"competitor_analysis": "Analyze competitor strategies and performance",
|
||||
"content_calendar": "Plan and schedule content publication",
|
||||
"content_ideas": "Generate creative content ideas and topics",
|
||||
"audience_analysis": "Research and define target audience",
|
||||
"content_planning": "Plan content strategy and themes",
|
||||
"social_content_creation": "Create platform-specific social media content",
|
||||
"hashtag_research": "Research relevant hashtags for social media",
|
||||
"technical_seo": "Analyze technical SEO aspects of website",
|
||||
"on_page_seo": "Review and optimize on-page SEO elements"
|
||||
}
|
||||
|
||||
return {
|
||||
"tool": step["tool"],
|
||||
"name": step["name"],
|
||||
"description": step_descriptions.get(step["tool"], "Execute this workflow step"),
|
||||
"step_number": step_index + 1,
|
||||
"total_steps": len(workflow["steps"])
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +0,0 @@
|
||||
"""
|
||||
UI Components for Enhanced ALwrity Chatbot.
|
||||
|
||||
This package contains modular UI components for the Streamlit interface:
|
||||
- sidebar: Intelligent sidebar with dashboard and quick tools
|
||||
"""
|
||||
|
||||
from .sidebar import SidebarManager
|
||||
|
||||
__all__ = [
|
||||
'SidebarManager'
|
||||
]
|
||||
@@ -1,396 +0,0 @@
|
||||
"""
|
||||
Sidebar Manager for Enhanced ALwrity Chatbot.
|
||||
|
||||
Manages the intelligent sidebar with dashboard, quick tools, and user analytics.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class SidebarManager:
|
||||
"""Manages the enhanced sidebar interface."""
|
||||
|
||||
def __init__(self, context_manager, workflow_engine, tool_router):
|
||||
self.context_manager = context_manager
|
||||
self.workflow_engine = workflow_engine
|
||||
self.tool_router = tool_router
|
||||
|
||||
def render_sidebar(self) -> Dict[str, Any]:
|
||||
"""Render the complete sidebar interface."""
|
||||
sidebar_data = {}
|
||||
|
||||
with st.sidebar:
|
||||
# Header
|
||||
st.markdown("# 🚀 ALwrity Hub")
|
||||
st.markdown("---")
|
||||
|
||||
# Dashboard section
|
||||
sidebar_data.update(self._render_dashboard())
|
||||
|
||||
# Quick tools section
|
||||
sidebar_data.update(self._render_quick_tools())
|
||||
|
||||
# Active workflows section
|
||||
sidebar_data.update(self._render_active_workflows())
|
||||
|
||||
# User preferences section
|
||||
sidebar_data.update(self._render_user_preferences())
|
||||
|
||||
# Analytics section
|
||||
sidebar_data.update(self._render_analytics())
|
||||
|
||||
# Export/Import section
|
||||
sidebar_data.update(self._render_export_import())
|
||||
|
||||
return sidebar_data
|
||||
|
||||
def _render_dashboard(self) -> Dict[str, Any]:
|
||||
"""Render the dashboard section."""
|
||||
st.markdown("## 📊 Dashboard")
|
||||
|
||||
# Get user analytics
|
||||
analytics = self.context_manager.get_user_analytics()
|
||||
|
||||
# Key metrics in columns
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.metric(
|
||||
label="Total Interactions",
|
||||
value=analytics.get("total_interactions", 0)
|
||||
)
|
||||
st.metric(
|
||||
label="Active Workflows",
|
||||
value=analytics.get("active_workflows_count", 0)
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.metric(
|
||||
label="Workflows Completed",
|
||||
value=analytics.get("workflows_completed", 0)
|
||||
)
|
||||
st.metric(
|
||||
label="Conversation Turns",
|
||||
value=analytics.get("conversation_turns", 0)
|
||||
)
|
||||
|
||||
# Most used tools
|
||||
most_used_tools = analytics.get("most_used_tools", [])
|
||||
if most_used_tools:
|
||||
st.markdown("**🔧 Most Used Tools:**")
|
||||
for tool, count in most_used_tools[:3]:
|
||||
st.markdown(f"• {tool}: {count} times")
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
return {"dashboard_rendered": True}
|
||||
|
||||
def _render_quick_tools(self) -> Dict[str, Any]:
|
||||
"""Render the quick tools section."""
|
||||
st.markdown("## ⚡ Quick Tools")
|
||||
|
||||
quick_actions = {}
|
||||
|
||||
# Content creation tools
|
||||
st.markdown("**✍️ Content Creation**")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
if st.button("📝 Blog Writer", key="quick_blog"):
|
||||
quick_actions["action"] = "blog_writer"
|
||||
if st.button("📱 Social Post", key="quick_social"):
|
||||
quick_actions["action"] = "social_post"
|
||||
|
||||
with col2:
|
||||
if st.button("📧 Email Writer", key="quick_email"):
|
||||
quick_actions["action"] = "email_writer"
|
||||
if st.button("📖 Story Writer", key="quick_story"):
|
||||
quick_actions["action"] = "story_writer"
|
||||
|
||||
# SEO tools
|
||||
st.markdown("**🔍 SEO Tools**")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
if st.button("🔧 Technical SEO", key="quick_tech_seo"):
|
||||
quick_actions["action"] = "technical_seo"
|
||||
if st.button("📊 Content Gap", key="quick_content_gap"):
|
||||
quick_actions["action"] = "content_gap"
|
||||
|
||||
with col2:
|
||||
if st.button("🎯 Keyword Research", key="quick_keywords"):
|
||||
quick_actions["action"] = "keyword_research"
|
||||
if st.button("🏆 Competitor Analysis", key="quick_competitor"):
|
||||
quick_actions["action"] = "competitor_analysis"
|
||||
|
||||
# Analysis tools
|
||||
st.markdown("**📈 Analysis**")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
if st.button("🌐 Website Analyzer", key="quick_website"):
|
||||
quick_actions["action"] = "website_analyzer"
|
||||
if st.button("📋 On-Page SEO", key="quick_onpage"):
|
||||
quick_actions["action"] = "onpage_seo"
|
||||
|
||||
with col2:
|
||||
if st.button("🔗 URL SEO Check", key="quick_url_seo"):
|
||||
quick_actions["action"] = "url_seo_check"
|
||||
if st.button("📱 Social Analyzer", key="quick_social_analyzer"):
|
||||
quick_actions["action"] = "social_analyzer"
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
return {"quick_actions": quick_actions}
|
||||
|
||||
def _render_active_workflows(self) -> Dict[str, Any]:
|
||||
"""Render the active workflows section."""
|
||||
st.markdown("## 🔄 Active Workflows")
|
||||
|
||||
workflow_actions = {}
|
||||
active_workflows = self.context_manager.get_active_workflows()
|
||||
paused_workflows = self.context_manager.get_paused_workflows()
|
||||
|
||||
if active_workflows:
|
||||
for workflow in active_workflows:
|
||||
with st.expander(f"🟢 {workflow.workflow_name}"):
|
||||
# Progress bar
|
||||
progress = workflow.current_step / workflow.total_steps
|
||||
st.progress(progress)
|
||||
st.markdown(f"Step {workflow.current_step}/{workflow.total_steps}")
|
||||
|
||||
# Action buttons
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
if st.button("⏸️ Pause", key=f"pause_{workflow.workflow_id}"):
|
||||
workflow_actions["pause"] = workflow.workflow_id
|
||||
with col2:
|
||||
if st.button("▶️ Continue", key=f"continue_{workflow.workflow_id}"):
|
||||
workflow_actions["continue"] = workflow.workflow_id
|
||||
|
||||
if paused_workflows:
|
||||
st.markdown("**⏸️ Paused Workflows:**")
|
||||
for workflow in paused_workflows:
|
||||
col1, col2 = st.columns([3, 1])
|
||||
with col1:
|
||||
st.markdown(f"• {workflow.workflow_name}")
|
||||
with col2:
|
||||
if st.button("▶️", key=f"resume_{workflow.workflow_id}"):
|
||||
workflow_actions["resume"] = workflow.workflow_id
|
||||
|
||||
# Start new workflow
|
||||
st.markdown("**🆕 Start New Workflow:**")
|
||||
available_workflows = list(self.workflow_engine.workflows.keys())
|
||||
selected_workflow = st.selectbox(
|
||||
"Choose workflow:",
|
||||
[""] + available_workflows,
|
||||
key="new_workflow_select"
|
||||
)
|
||||
|
||||
if selected_workflow and st.button("🚀 Start Workflow", key="start_new_workflow"):
|
||||
workflow_actions["start"] = selected_workflow
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
return {"workflow_actions": workflow_actions}
|
||||
|
||||
def _render_user_preferences(self) -> Dict[str, Any]:
|
||||
"""Render the user preferences section."""
|
||||
st.markdown("## ⚙️ Preferences")
|
||||
|
||||
preferences_updated = {}
|
||||
current_prefs = self.context_manager.user_preferences
|
||||
|
||||
with st.expander("🎨 Content Preferences"):
|
||||
# Tone preference
|
||||
tone = st.selectbox(
|
||||
"Preferred Tone:",
|
||||
["professional", "casual", "friendly", "formal", "creative"],
|
||||
index=["professional", "casual", "friendly", "formal", "creative"].index(
|
||||
current_prefs.preferred_tone
|
||||
),
|
||||
key="pref_tone"
|
||||
)
|
||||
|
||||
# Length preference
|
||||
length = st.selectbox(
|
||||
"Preferred Length:",
|
||||
["short", "medium", "long", "comprehensive"],
|
||||
index=["short", "medium", "long", "comprehensive"].index(
|
||||
current_prefs.preferred_length
|
||||
),
|
||||
key="pref_length"
|
||||
)
|
||||
|
||||
# Industry focus
|
||||
industry_focus = st.multiselect(
|
||||
"Industry Focus:",
|
||||
["Technology", "Healthcare", "Finance", "Education", "Marketing",
|
||||
"E-commerce", "Travel", "Food", "Fashion", "Real Estate"],
|
||||
default=current_prefs.industry_focus,
|
||||
key="pref_industry"
|
||||
)
|
||||
|
||||
# Content preferences
|
||||
content_prefs = st.multiselect(
|
||||
"Content Types:",
|
||||
["Blog Posts", "Social Media", "Email Marketing", "Technical Writing",
|
||||
"Creative Writing", "SEO Content", "Product Descriptions", "News Articles"],
|
||||
default=current_prefs.content_preferences,
|
||||
key="pref_content_types"
|
||||
)
|
||||
|
||||
if st.button("💾 Save Preferences", key="save_preferences"):
|
||||
preferences_updated = {
|
||||
"preferred_tone": tone,
|
||||
"preferred_length": length,
|
||||
"industry_focus": industry_focus,
|
||||
"content_preferences": content_prefs
|
||||
}
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
return {"preferences_updated": preferences_updated}
|
||||
|
||||
def _render_analytics(self) -> Dict[str, Any]:
|
||||
"""Render the analytics section."""
|
||||
st.markdown("## 📈 Analytics")
|
||||
|
||||
analytics = self.context_manager.get_user_analytics()
|
||||
|
||||
with st.expander("📊 Usage Statistics"):
|
||||
# Recent activity pattern
|
||||
recent_activity = analytics.get("recent_activity_pattern", {})
|
||||
if recent_activity:
|
||||
st.markdown("**Recent Activity:**")
|
||||
for date, count in list(recent_activity.items())[-7:]: # Last 7 days
|
||||
st.markdown(f"• {date}: {count} interactions")
|
||||
|
||||
# Tool usage breakdown
|
||||
most_used_tools = analytics.get("most_used_tools", [])
|
||||
if most_used_tools:
|
||||
st.markdown("**Tool Usage Breakdown:**")
|
||||
for tool, count in most_used_tools:
|
||||
percentage = (count / analytics.get("total_interactions", 1)) * 100
|
||||
st.markdown(f"• {tool}: {count} ({percentage:.1f}%)")
|
||||
|
||||
# Context summary
|
||||
with st.expander("🧠 Context Summary"):
|
||||
context_summary = self.context_manager.get_context_summary()
|
||||
st.text(context_summary)
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
return {"analytics_viewed": True}
|
||||
|
||||
def _render_export_import(self) -> Dict[str, Any]:
|
||||
"""Render the export/import section."""
|
||||
st.markdown("## 💾 Data Management")
|
||||
|
||||
export_actions = {}
|
||||
|
||||
with st.expander("📤 Export Data"):
|
||||
export_format = st.selectbox(
|
||||
"Export Format:",
|
||||
["JSON", "TXT"],
|
||||
key="export_format"
|
||||
)
|
||||
|
||||
if st.button("📥 Export Conversation History", key="export_history"):
|
||||
export_actions["export"] = {
|
||||
"type": "conversation_history",
|
||||
"format": export_format.lower()
|
||||
}
|
||||
|
||||
if st.button("📊 Export Analytics", key="export_analytics"):
|
||||
export_actions["export"] = {
|
||||
"type": "analytics",
|
||||
"format": export_format.lower()
|
||||
}
|
||||
|
||||
with st.expander("🗑️ Data Cleanup"):
|
||||
cleanup_days = st.number_input(
|
||||
"Keep data for (days):",
|
||||
min_value=1,
|
||||
max_value=365,
|
||||
value=30,
|
||||
key="cleanup_days"
|
||||
)
|
||||
|
||||
if st.button("🧹 Cleanup Old Data", key="cleanup_data"):
|
||||
export_actions["cleanup"] = cleanup_days
|
||||
|
||||
if st.button("⚠️ Reset All Data", key="reset_data"):
|
||||
if st.checkbox("I understand this will delete all data", key="confirm_reset"):
|
||||
export_actions["reset"] = True
|
||||
|
||||
return {"export_actions": export_actions}
|
||||
|
||||
def render_workflow_suggestions(self, intent_analysis: Dict[str, Any]) -> Optional[str]:
|
||||
"""Render workflow suggestions based on intent analysis."""
|
||||
suggested_workflows = intent_analysis.get("suggested_workflows", [])
|
||||
|
||||
if suggested_workflows:
|
||||
st.sidebar.markdown("## 💡 Suggested Workflows")
|
||||
|
||||
for workflow in suggested_workflows[:3]: # Show top 3 suggestions
|
||||
workflow_info = self.workflow_engine.get_workflow(workflow)
|
||||
if workflow_info:
|
||||
with st.sidebar.expander(f"🔄 {workflow_info['name']}"):
|
||||
st.markdown(f"**Description:** {workflow_info['description']}")
|
||||
st.markdown(f"**Steps:** {len(workflow_info['steps'])}")
|
||||
|
||||
if st.button(f"Start {workflow_info['name']}",
|
||||
key=f"suggest_{workflow}"):
|
||||
return workflow
|
||||
|
||||
return None
|
||||
|
||||
def render_tool_suggestions(self, intent_analysis: Dict[str, Any]) -> Optional[str]:
|
||||
"""Render tool suggestions based on intent analysis."""
|
||||
suggested_tools = intent_analysis.get("suggested_tools", [])
|
||||
|
||||
if suggested_tools:
|
||||
st.sidebar.markdown("## 🛠️ Suggested Tools")
|
||||
|
||||
# Group tools by category
|
||||
tool_categories = self.tool_router.tool_categories
|
||||
categorized_tools = {}
|
||||
|
||||
for tool in suggested_tools[:6]: # Show top 6 suggestions
|
||||
for category, tools in tool_categories.items():
|
||||
if tool in tools:
|
||||
if category not in categorized_tools:
|
||||
categorized_tools[category] = []
|
||||
categorized_tools[category].append(tool)
|
||||
break
|
||||
|
||||
for category, tools in categorized_tools.items():
|
||||
st.sidebar.markdown(f"**{category.title()}:**")
|
||||
for tool in tools:
|
||||
if st.sidebar.button(f"🚀 {tool.replace('_', ' ').title()}",
|
||||
key=f"suggest_tool_{tool}"):
|
||||
return tool
|
||||
|
||||
return None
|
||||
|
||||
def show_notification(self, message: str, type: str = "info"):
|
||||
"""Show a notification in the sidebar."""
|
||||
if type == "success":
|
||||
st.sidebar.success(message)
|
||||
elif type == "error":
|
||||
st.sidebar.error(message)
|
||||
elif type == "warning":
|
||||
st.sidebar.warning(message)
|
||||
else:
|
||||
st.sidebar.info(message)
|
||||
|
||||
def get_sidebar_state(self) -> Dict[str, Any]:
|
||||
"""Get current sidebar state for persistence."""
|
||||
return {
|
||||
"last_updated": datetime.now().isoformat(),
|
||||
"active_sections": st.session_state.get("sidebar_sections", []),
|
||||
"user_preferences": self.context_manager.user_preferences.__dict__
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
"""
|
||||
Database Package for ALwrity
|
||||
============================
|
||||
|
||||
This package provides database models and services for managing data
|
||||
in the ALwrity application, including Twitter-specific functionality.
|
||||
|
||||
Main Components:
|
||||
- models.py: Core application database models
|
||||
- twitter_models.py: Twitter-specific database models
|
||||
- twitter_service.py: High-level Twitter database service
|
||||
- twitter_init.py: Database initialization and management utilities
|
||||
|
||||
Usage:
|
||||
# Initialize Twitter database
|
||||
from lib.database import initialize_twitter_database
|
||||
initialize_twitter_database()
|
||||
|
||||
# Use Twitter database service
|
||||
from lib.database import twitter_db
|
||||
user = twitter_db.create_or_update_user(user_data)
|
||||
|
||||
# Use Twitter models directly
|
||||
from lib.database.twitter_models import TwitterUser, Tweet
|
||||
"""
|
||||
|
||||
# Import core models
|
||||
from .models import (
|
||||
SEOData, ContentType, Platform, ScheduleStatus,
|
||||
ContentItem, Schedule, create_engine, init_db, get_session
|
||||
)
|
||||
|
||||
# Import Twitter-specific components
|
||||
try:
|
||||
from .twitter_models import (
|
||||
# Models
|
||||
TwitterUser, Tweet, ScheduledTweet, TwitterAnalytics,
|
||||
TweetAnalytics, EngagementData, AudienceInsight,
|
||||
HashtagPerformance, ContentTemplate, TwitterSettings,
|
||||
|
||||
# Enums and Data Classes
|
||||
TwitterAccountType, TweetType, TweetStatus, EngagementType,
|
||||
AnalyticsTimeframe, ContentCategory, TwitterCredentials, TweetMetrics,
|
||||
|
||||
# Database functions
|
||||
get_twitter_engine, init_twitter_db, get_twitter_session,
|
||||
create_twitter_user, update_user_metrics, create_tweet_record,
|
||||
update_tweet_metrics, calculate_virality_score, get_user_analytics_summary
|
||||
)
|
||||
|
||||
from .twitter_service import TwitterDatabaseService, twitter_db
|
||||
|
||||
from .twitter_init import (
|
||||
TwitterDatabaseInitializer, initialize_twitter_database,
|
||||
check_twitter_database_health
|
||||
)
|
||||
|
||||
TWITTER_AVAILABLE = True
|
||||
|
||||
except ImportError as e:
|
||||
# Twitter components not available (missing dependencies)
|
||||
TWITTER_AVAILABLE = False
|
||||
print(f"Warning: Twitter database components not available: {e}")
|
||||
|
||||
# Package metadata
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "ALwrity Team"
|
||||
|
||||
# Export main components
|
||||
__all__ = [
|
||||
# Core models
|
||||
'SEOData', 'ContentType', 'Platform', 'ScheduleStatus',
|
||||
'ContentItem', 'Schedule', 'create_engine', 'init_db', 'get_session',
|
||||
|
||||
# Twitter availability flag
|
||||
'TWITTER_AVAILABLE',
|
||||
]
|
||||
|
||||
# Add Twitter exports if available
|
||||
if TWITTER_AVAILABLE:
|
||||
__all__.extend([
|
||||
# Twitter Models
|
||||
'TwitterUser', 'Tweet', 'ScheduledTweet', 'TwitterAnalytics',
|
||||
'TweetAnalytics', 'EngagementData', 'AudienceInsight',
|
||||
'HashtagPerformance', 'ContentTemplate', 'TwitterSettings',
|
||||
|
||||
# Twitter Enums and Data Classes
|
||||
'TwitterAccountType', 'TweetType', 'TweetStatus', 'EngagementType',
|
||||
'AnalyticsTimeframe', 'ContentCategory', 'TwitterCredentials', 'TweetMetrics',
|
||||
|
||||
# Twitter Database Functions
|
||||
'get_twitter_engine', 'init_twitter_db', 'get_twitter_session',
|
||||
'create_twitter_user', 'update_user_metrics', 'create_tweet_record',
|
||||
'update_tweet_metrics', 'calculate_virality_score', 'get_user_analytics_summary',
|
||||
|
||||
# Twitter Service
|
||||
'TwitterDatabaseService', 'twitter_db',
|
||||
|
||||
# Twitter Initialization
|
||||
'TwitterDatabaseInitializer', 'initialize_twitter_database',
|
||||
'check_twitter_database_health'
|
||||
])
|
||||
|
||||
def setup_database(db_url: str = "sqlite:///alwrity.db", twitter_db_url: str = "sqlite:///twitter_data.db"):
|
||||
"""
|
||||
Setup both core and Twitter databases.
|
||||
|
||||
Args:
|
||||
db_url: URL for the core database
|
||||
twitter_db_url: URL for the Twitter database
|
||||
|
||||
Returns:
|
||||
dict: Setup results
|
||||
"""
|
||||
results = {
|
||||
'core_db': False,
|
||||
'twitter_db': False,
|
||||
'errors': []
|
||||
}
|
||||
|
||||
try:
|
||||
# Initialize core database
|
||||
engine = create_engine(db_url)
|
||||
init_db(engine)
|
||||
results['core_db'] = True
|
||||
except Exception as e:
|
||||
results['errors'].append(f"Core database setup failed: {e}")
|
||||
|
||||
if TWITTER_AVAILABLE:
|
||||
try:
|
||||
# Initialize Twitter database
|
||||
success = initialize_twitter_database(twitter_db_url)
|
||||
results['twitter_db'] = success
|
||||
if not success:
|
||||
results['errors'].append("Twitter database initialization failed")
|
||||
except Exception as e:
|
||||
results['errors'].append(f"Twitter database setup failed: {e}")
|
||||
else:
|
||||
results['errors'].append("Twitter database components not available")
|
||||
|
||||
return results
|
||||
|
||||
def get_database_info():
|
||||
"""
|
||||
Get information about available database components.
|
||||
|
||||
Returns:
|
||||
dict: Database component information
|
||||
"""
|
||||
info = {
|
||||
'core_models_available': True,
|
||||
'twitter_models_available': TWITTER_AVAILABLE,
|
||||
'version': __version__
|
||||
}
|
||||
|
||||
if TWITTER_AVAILABLE:
|
||||
try:
|
||||
# Get Twitter database stats if service is available
|
||||
stats = twitter_db.get_database_stats()
|
||||
info['twitter_stats'] = stats
|
||||
except Exception as e:
|
||||
info['twitter_stats_error'] = str(e)
|
||||
|
||||
return info
|
||||
@@ -1,105 +0,0 @@
|
||||
from sqlalchemy import (
|
||||
create_engine, Column, Integer, String, Text, DateTime, Enum, ForeignKey, JSON
|
||||
)
|
||||
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
|
||||
from datetime import datetime
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict, Any
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
# --- DATACLASSES ---
|
||||
|
||||
@dataclass
|
||||
class SEOData:
|
||||
title: str = ""
|
||||
meta_description: str = ""
|
||||
keywords: List[str] = None
|
||||
structured_data: Dict[str, Any] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.keywords is None:
|
||||
self.keywords = []
|
||||
if self.structured_data is None:
|
||||
self.structured_data = {}
|
||||
|
||||
# --- ENUMS ---
|
||||
|
||||
class ContentType(enum.Enum):
|
||||
BLOG_POST = "blog_post"
|
||||
SOCIAL_MEDIA = "social_media"
|
||||
VIDEO = "video"
|
||||
NEWSLETTER = "newsletter"
|
||||
|
||||
class Platform(enum.Enum):
|
||||
WEBSITE = "website"
|
||||
INSTAGRAM = "instagram"
|
||||
TWITTER = "twitter"
|
||||
LINKEDIN = "linkedin"
|
||||
FACEBOOK = "facebook"
|
||||
|
||||
class ScheduleStatus(enum.Enum):
|
||||
SCHEDULED = "scheduled"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
# --- MODELS ---
|
||||
|
||||
class ContentItem(Base):
|
||||
__tablename__ = "content_items"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(Text)
|
||||
content_type = Column(Enum(ContentType), nullable=False)
|
||||
platforms = Column(JSON, nullable=False) # List of platforms (as strings)
|
||||
publish_date = Column(DateTime, nullable=False)
|
||||
status = Column(String, default="draft")
|
||||
author = Column(String)
|
||||
tags = Column(JSON, default=list)
|
||||
notes = Column(Text)
|
||||
seo_data = Column(JSON, default=dict)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
schedules = relationship("Schedule", back_populates="content_item", cascade="all, delete-orphan")
|
||||
|
||||
class Schedule(Base):
|
||||
__tablename__ = "schedules"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
content_item_id = Column(Integer, ForeignKey("content_items.id"), nullable=False)
|
||||
scheduled_time = Column(DateTime, nullable=False)
|
||||
status = Column(Enum(ScheduleStatus), default=ScheduleStatus.SCHEDULED)
|
||||
recurrence = Column(String) # e.g., 'none', 'daily', 'weekly'
|
||||
priority = Column(Integer, default=1)
|
||||
result = Column(Text)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
content_item = relationship("ContentItem", back_populates="schedules")
|
||||
|
||||
# --- DB INIT & SESSION ---
|
||||
|
||||
def get_engine(db_url="sqlite:///content_scheduler.db"):
|
||||
return create_engine(db_url, echo=False)
|
||||
|
||||
def init_db(engine):
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
def get_session(engine):
|
||||
Session = sessionmaker(bind=engine)
|
||||
return Session()
|
||||
|
||||
__all__ = [
|
||||
'ContentItem',
|
||||
'ContentType',
|
||||
'Platform',
|
||||
'SEOData',
|
||||
'get_engine',
|
||||
'get_session',
|
||||
'init_db',
|
||||
]
|
||||
@@ -1,524 +0,0 @@
|
||||
"""
|
||||
Twitter Database Initialization and Migration Script
|
||||
===================================================
|
||||
|
||||
This module provides utilities for initializing the Twitter database,
|
||||
handling schema migrations, and managing database setup.
|
||||
|
||||
Features:
|
||||
- Database initialization and table creation
|
||||
- Schema migration utilities
|
||||
- Data seeding for development/testing
|
||||
- Database health checks and maintenance
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import create_engine, text, inspect
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from .twitter_models import (
|
||||
Base, TwitterUser, Tweet, ScheduledTweet, TwitterAnalytics,
|
||||
TweetAnalytics, EngagementData, AudienceInsight, HashtagPerformance,
|
||||
ContentTemplate, TwitterSettings, TwitterAccountType, TweetType,
|
||||
TweetStatus, EngagementType, AnalyticsTimeframe, ContentCategory
|
||||
)
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TwitterDatabaseInitializer:
|
||||
"""
|
||||
Handles Twitter database initialization and management.
|
||||
"""
|
||||
|
||||
def __init__(self, db_url: str = "sqlite:///twitter_data.db"):
|
||||
"""Initialize the database initializer."""
|
||||
self.db_url = db_url
|
||||
self.engine = create_engine(db_url, echo=False)
|
||||
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
|
||||
|
||||
# Create database directory if using SQLite
|
||||
if db_url.startswith('sqlite:///'):
|
||||
db_path = db_url.replace('sqlite:///', '')
|
||||
os.makedirs(os.path.dirname(os.path.abspath(db_path)), exist_ok=True)
|
||||
|
||||
def initialize_database(self, force_recreate: bool = False) -> bool:
|
||||
"""
|
||||
Initialize the Twitter database with all required tables.
|
||||
|
||||
Args:
|
||||
force_recreate: If True, drop existing tables and recreate
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
if force_recreate:
|
||||
logger.info("Dropping existing tables...")
|
||||
Base.metadata.drop_all(bind=self.engine)
|
||||
|
||||
logger.info("Creating Twitter database tables...")
|
||||
Base.metadata.create_all(bind=self.engine)
|
||||
|
||||
# Verify tables were created
|
||||
inspector = inspect(self.engine)
|
||||
tables = inspector.get_table_names()
|
||||
|
||||
expected_tables = [
|
||||
'twitter_users', 'tweets', 'scheduled_tweets', 'twitter_analytics',
|
||||
'tweet_analytics', 'engagement_data', 'audience_insights',
|
||||
'hashtag_performance', 'content_templates', 'twitter_settings'
|
||||
]
|
||||
|
||||
missing_tables = [table for table in expected_tables if table not in tables]
|
||||
|
||||
if missing_tables:
|
||||
logger.error(f"Missing tables: {missing_tables}")
|
||||
return False
|
||||
|
||||
logger.info(f"Successfully created {len(tables)} tables")
|
||||
|
||||
# Create indexes for better performance
|
||||
self._create_indexes()
|
||||
|
||||
# Seed initial data if needed
|
||||
self._seed_initial_data()
|
||||
|
||||
logger.info("Twitter database initialization completed successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing database: {e}")
|
||||
return False
|
||||
|
||||
def _create_indexes(self):
|
||||
"""Create database indexes for better query performance."""
|
||||
try:
|
||||
with self.engine.connect() as conn:
|
||||
# User indexes
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_twitter_users_user_id ON twitter_users(user_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_twitter_users_twitter_user_id ON twitter_users(twitter_user_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_twitter_users_username ON twitter_users(username)"))
|
||||
|
||||
# Tweet indexes
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_tweets_user_id ON tweets(user_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_tweets_status ON tweets(status)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_tweets_posted_at ON tweets(posted_at)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_tweets_tweet_id ON tweets(tweet_id)"))
|
||||
|
||||
# Scheduled tweet indexes
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_scheduled_tweets_user_id ON scheduled_tweets(user_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_scheduled_tweets_status ON scheduled_tweets(status)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_scheduled_tweets_scheduled_time ON scheduled_tweets(scheduled_time)"))
|
||||
|
||||
# Analytics indexes
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_twitter_analytics_user_id ON twitter_analytics(user_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_twitter_analytics_date ON twitter_analytics(date)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_twitter_analytics_timeframe ON twitter_analytics(timeframe)"))
|
||||
|
||||
# Tweet analytics indexes
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_tweet_analytics_tweet_id ON tweet_analytics(tweet_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_tweet_analytics_recorded_at ON tweet_analytics(recorded_at)"))
|
||||
|
||||
# Engagement data indexes
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_engagement_data_tweet_id ON engagement_data(tweet_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_engagement_data_occurred_at ON engagement_data(occurred_at)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_engagement_data_type ON engagement_data(engagement_type)"))
|
||||
|
||||
# Hashtag performance indexes
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_hashtag_performance_user_id ON hashtag_performance(user_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_hashtag_performance_hashtag ON hashtag_performance(hashtag)"))
|
||||
|
||||
# Content template indexes
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_content_templates_user_id ON content_templates(user_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_content_templates_category ON content_templates(category)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_content_templates_is_active ON content_templates(is_active)"))
|
||||
|
||||
conn.commit()
|
||||
logger.info("Database indexes created successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating indexes: {e}")
|
||||
|
||||
def _seed_initial_data(self):
|
||||
"""Seed the database with initial data for development/testing."""
|
||||
try:
|
||||
session = self.SessionLocal()
|
||||
|
||||
# Check if we already have data
|
||||
if session.query(TwitterUser).count() > 0:
|
||||
logger.info("Database already contains data, skipping seeding")
|
||||
session.close()
|
||||
return
|
||||
|
||||
# Create sample content templates
|
||||
sample_templates = [
|
||||
{
|
||||
'name': 'Daily Motivation',
|
||||
'description': 'Motivational quotes and thoughts',
|
||||
'template_text': 'Start your day with this thought: {quote} #motivation #success',
|
||||
'category': ContentCategory.PERSONAL,
|
||||
'variables': ['quote'],
|
||||
'default_hashtags': ['#motivation', '#success', '#mindset'],
|
||||
'ai_prompt': 'Generate an inspiring motivational quote',
|
||||
'ai_tone': 'inspirational',
|
||||
'ai_target_audience': 'professionals and entrepreneurs'
|
||||
},
|
||||
{
|
||||
'name': 'Tech News Share',
|
||||
'description': 'Template for sharing tech news',
|
||||
'template_text': 'Interesting development in {topic}: {summary} {link} #tech #innovation',
|
||||
'category': ContentCategory.EDUCATIONAL,
|
||||
'variables': ['topic', 'summary', 'link'],
|
||||
'default_hashtags': ['#tech', '#innovation', '#technology'],
|
||||
'ai_prompt': 'Summarize this tech news in an engaging way',
|
||||
'ai_tone': 'informative',
|
||||
'ai_target_audience': 'tech enthusiasts and professionals'
|
||||
},
|
||||
{
|
||||
'name': 'Question Engagement',
|
||||
'description': 'Template for asking engaging questions',
|
||||
'template_text': 'Quick question for my followers: {question} What do you think? #community #discussion',
|
||||
'category': ContentCategory.QUESTION,
|
||||
'variables': ['question'],
|
||||
'default_hashtags': ['#community', '#discussion', '#question'],
|
||||
'ai_prompt': 'Generate an engaging question for social media',
|
||||
'ai_tone': 'conversational',
|
||||
'ai_target_audience': 'general audience'
|
||||
},
|
||||
{
|
||||
'name': 'Product Update',
|
||||
'description': 'Template for product announcements',
|
||||
'template_text': 'Excited to share: {update} {details} #product #update #announcement',
|
||||
'category': ContentCategory.PROMOTIONAL,
|
||||
'variables': ['update', 'details'],
|
||||
'default_hashtags': ['#product', '#update', '#announcement'],
|
||||
'ai_prompt': 'Write an exciting product update announcement',
|
||||
'ai_tone': 'enthusiastic',
|
||||
'ai_target_audience': 'customers and prospects'
|
||||
}
|
||||
]
|
||||
|
||||
# Note: We can't create templates without a user, so we'll skip this for now
|
||||
# In a real scenario, templates would be created when users are added
|
||||
|
||||
session.close()
|
||||
logger.info("Initial data seeding completed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error seeding initial data: {e}")
|
||||
|
||||
def check_database_health(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Check the health and status of the Twitter database.
|
||||
|
||||
Returns:
|
||||
Dict containing health check results
|
||||
"""
|
||||
health_status = {
|
||||
'status': 'healthy',
|
||||
'timestamp': datetime.utcnow().isoformat(),
|
||||
'tables': {},
|
||||
'indexes': {},
|
||||
'issues': []
|
||||
}
|
||||
|
||||
try:
|
||||
inspector = inspect(self.engine)
|
||||
|
||||
# Check table existence and row counts
|
||||
expected_tables = [
|
||||
'twitter_users', 'tweets', 'scheduled_tweets', 'twitter_analytics',
|
||||
'tweet_analytics', 'engagement_data', 'audience_insights',
|
||||
'hashtag_performance', 'content_templates', 'twitter_settings'
|
||||
]
|
||||
|
||||
session = self.SessionLocal()
|
||||
|
||||
for table_name in expected_tables:
|
||||
if table_name in inspector.get_table_names():
|
||||
# Get row count
|
||||
try:
|
||||
result = session.execute(text(f"SELECT COUNT(*) FROM {table_name}"))
|
||||
count = result.scalar()
|
||||
health_status['tables'][table_name] = {
|
||||
'exists': True,
|
||||
'row_count': count
|
||||
}
|
||||
except Exception as e:
|
||||
health_status['tables'][table_name] = {
|
||||
'exists': True,
|
||||
'row_count': 'error',
|
||||
'error': str(e)
|
||||
}
|
||||
health_status['issues'].append(f"Error counting rows in {table_name}: {e}")
|
||||
else:
|
||||
health_status['tables'][table_name] = {'exists': False}
|
||||
health_status['issues'].append(f"Missing table: {table_name}")
|
||||
|
||||
# Check indexes
|
||||
for table_name in inspector.get_table_names():
|
||||
indexes = inspector.get_indexes(table_name)
|
||||
health_status['indexes'][table_name] = len(indexes)
|
||||
|
||||
session.close()
|
||||
|
||||
# Set overall status
|
||||
if health_status['issues']:
|
||||
health_status['status'] = 'issues_found'
|
||||
|
||||
return health_status
|
||||
|
||||
except Exception as e:
|
||||
health_status['status'] = 'error'
|
||||
health_status['error'] = str(e)
|
||||
logger.error(f"Error checking database health: {e}")
|
||||
return health_status
|
||||
|
||||
def backup_database(self, backup_path: str) -> bool:
|
||||
"""
|
||||
Create a backup of the database.
|
||||
|
||||
Args:
|
||||
backup_path: Path where to save the backup
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
if not self.db_url.startswith('sqlite:///'):
|
||||
logger.error("Backup currently only supported for SQLite databases")
|
||||
return False
|
||||
|
||||
# Get the database file path
|
||||
db_file = self.db_url.replace('sqlite:///', '')
|
||||
|
||||
if not os.path.exists(db_file):
|
||||
logger.error(f"Database file not found: {db_file}")
|
||||
return False
|
||||
|
||||
# Create backup directory if it doesn't exist
|
||||
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
|
||||
|
||||
# Copy the database file
|
||||
import shutil
|
||||
shutil.copy2(db_file, backup_path)
|
||||
|
||||
logger.info(f"Database backed up to: {backup_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error backing up database: {e}")
|
||||
return False
|
||||
|
||||
def restore_database(self, backup_path: str) -> bool:
|
||||
"""
|
||||
Restore database from a backup.
|
||||
|
||||
Args:
|
||||
backup_path: Path to the backup file
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
if not self.db_url.startswith('sqlite:///'):
|
||||
logger.error("Restore currently only supported for SQLite databases")
|
||||
return False
|
||||
|
||||
if not os.path.exists(backup_path):
|
||||
logger.error(f"Backup file not found: {backup_path}")
|
||||
return False
|
||||
|
||||
# Get the database file path
|
||||
db_file = self.db_url.replace('sqlite:///', '')
|
||||
|
||||
# Copy the backup file to the database location
|
||||
import shutil
|
||||
shutil.copy2(backup_path, db_file)
|
||||
|
||||
logger.info(f"Database restored from: {backup_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error restoring database: {e}")
|
||||
return False
|
||||
|
||||
def migrate_schema(self, migration_scripts: List[str]) -> bool:
|
||||
"""
|
||||
Apply schema migration scripts.
|
||||
|
||||
Args:
|
||||
migration_scripts: List of SQL migration scripts
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
with self.engine.connect() as conn:
|
||||
# Create migration tracking table if it doesn't exist
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
migration_name TEXT NOT NULL UNIQUE,
|
||||
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
|
||||
for script in migration_scripts:
|
||||
# Check if migration was already applied
|
||||
result = conn.execute(text(
|
||||
"SELECT COUNT(*) FROM schema_migrations WHERE migration_name = :name"
|
||||
), {"name": script})
|
||||
|
||||
if result.scalar() == 0:
|
||||
# Apply migration
|
||||
logger.info(f"Applying migration: {script}")
|
||||
|
||||
# Read and execute migration script
|
||||
script_path = Path(script)
|
||||
if script_path.exists():
|
||||
with open(script_path, 'r') as f:
|
||||
migration_sql = f.read()
|
||||
|
||||
conn.execute(text(migration_sql))
|
||||
|
||||
# Record migration as applied
|
||||
conn.execute(text(
|
||||
"INSERT INTO schema_migrations (migration_name) VALUES (:name)"
|
||||
), {"name": script})
|
||||
else:
|
||||
logger.error(f"Migration script not found: {script}")
|
||||
return False
|
||||
else:
|
||||
logger.info(f"Migration already applied: {script}")
|
||||
|
||||
conn.commit()
|
||||
logger.info("Schema migration completed successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error applying schema migration: {e}")
|
||||
return False
|
||||
|
||||
def cleanup_old_data(self, days: int = 90) -> Dict[str, int]:
|
||||
"""
|
||||
Clean up old data to maintain database performance.
|
||||
|
||||
Args:
|
||||
days: Number of days to keep data for
|
||||
|
||||
Returns:
|
||||
Dict with cleanup statistics
|
||||
"""
|
||||
try:
|
||||
cutoff_date = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
cutoff_date = cutoff_date.replace(day=cutoff_date.day - days)
|
||||
|
||||
session = self.SessionLocal()
|
||||
|
||||
# Count records to be deleted
|
||||
old_tweet_analytics = session.query(TweetAnalytics).filter(
|
||||
TweetAnalytics.recorded_at < cutoff_date
|
||||
).count()
|
||||
|
||||
old_engagement_data = session.query(EngagementData).filter(
|
||||
EngagementData.occurred_at < cutoff_date
|
||||
).count()
|
||||
|
||||
# Delete old records
|
||||
session.query(TweetAnalytics).filter(
|
||||
TweetAnalytics.recorded_at < cutoff_date
|
||||
).delete()
|
||||
|
||||
session.query(EngagementData).filter(
|
||||
EngagementData.occurred_at < cutoff_date
|
||||
).delete()
|
||||
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
cleanup_stats = {
|
||||
'tweet_analytics_deleted': old_tweet_analytics,
|
||||
'engagement_data_deleted': old_engagement_data,
|
||||
'cutoff_date': cutoff_date.isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"Cleanup completed: {cleanup_stats}")
|
||||
return cleanup_stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during cleanup: {e}")
|
||||
return {'error': str(e)}
|
||||
|
||||
def initialize_twitter_database(db_url: str = "sqlite:///twitter_data.db", force_recreate: bool = False) -> bool:
|
||||
"""
|
||||
Convenience function to initialize the Twitter database.
|
||||
|
||||
Args:
|
||||
db_url: Database URL
|
||||
force_recreate: Whether to recreate existing tables
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
initializer = TwitterDatabaseInitializer(db_url)
|
||||
return initializer.initialize_database(force_recreate)
|
||||
|
||||
def check_twitter_database_health(db_url: str = "sqlite:///twitter_data.db") -> Dict[str, Any]:
|
||||
"""
|
||||
Convenience function to check Twitter database health.
|
||||
|
||||
Args:
|
||||
db_url: Database URL
|
||||
|
||||
Returns:
|
||||
Dict with health check results
|
||||
"""
|
||||
initializer = TwitterDatabaseInitializer(db_url)
|
||||
return initializer.check_database_health()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Command line interface for database management
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Twitter Database Management")
|
||||
parser.add_argument("--db-url", default="sqlite:///twitter_data.db", help="Database URL")
|
||||
parser.add_argument("--init", action="store_true", help="Initialize database")
|
||||
parser.add_argument("--force", action="store_true", help="Force recreate tables")
|
||||
parser.add_argument("--health", action="store_true", help="Check database health")
|
||||
parser.add_argument("--backup", help="Create database backup")
|
||||
parser.add_argument("--restore", help="Restore from backup")
|
||||
parser.add_argument("--cleanup", type=int, help="Cleanup data older than N days")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
initializer = TwitterDatabaseInitializer(args.db_url)
|
||||
|
||||
if args.init:
|
||||
success = initializer.initialize_database(args.force)
|
||||
print(f"Database initialization: {'SUCCESS' if success else 'FAILED'}")
|
||||
|
||||
if args.health:
|
||||
health = initializer.check_database_health()
|
||||
print(json.dumps(health, indent=2))
|
||||
|
||||
if args.backup:
|
||||
success = initializer.backup_database(args.backup)
|
||||
print(f"Database backup: {'SUCCESS' if success else 'FAILED'}")
|
||||
|
||||
if args.restore:
|
||||
success = initializer.restore_database(args.restore)
|
||||
print(f"Database restore: {'SUCCESS' if success else 'FAILED'}")
|
||||
|
||||
if args.cleanup:
|
||||
stats = initializer.cleanup_old_data(args.cleanup)
|
||||
print(f"Cleanup completed: {stats}")
|
||||
@@ -1,791 +0,0 @@
|
||||
"""
|
||||
Twitter Database Models for ALwrity
|
||||
===================================
|
||||
|
||||
This module defines SQLAlchemy models for storing Twitter-related data including:
|
||||
- User profiles and authentication
|
||||
- Tweet content and metadata
|
||||
- Analytics and engagement metrics
|
||||
- Scheduling and automation data
|
||||
- Performance tracking and insights
|
||||
|
||||
This allows the application to store Twitter data locally and reduce API calls
|
||||
while providing rich analytics and historical data to users.
|
||||
"""
|
||||
|
||||
from sqlalchemy import (
|
||||
create_engine, Column, Integer, String, Text, DateTime, Boolean, Float,
|
||||
Enum, ForeignKey, JSON, BigInteger, Index, UniqueConstraint
|
||||
)
|
||||
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
|
||||
from datetime import datetime, timedelta
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict, Any, Optional
|
||||
import json
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
# --- ENUMS ---
|
||||
|
||||
class TwitterAccountType(enum.Enum):
|
||||
PERSONAL = "personal"
|
||||
BUSINESS = "business"
|
||||
CREATOR = "creator"
|
||||
BRAND = "brand"
|
||||
|
||||
class TweetType(enum.Enum):
|
||||
ORIGINAL = "original"
|
||||
REPLY = "reply"
|
||||
RETWEET = "retweet"
|
||||
QUOTE_TWEET = "quote_tweet"
|
||||
THREAD = "thread"
|
||||
|
||||
class TweetStatus(enum.Enum):
|
||||
DRAFT = "draft"
|
||||
SCHEDULED = "scheduled"
|
||||
POSTED = "posted"
|
||||
FAILED = "failed"
|
||||
DELETED = "deleted"
|
||||
|
||||
class EngagementType(enum.Enum):
|
||||
LIKE = "like"
|
||||
RETWEET = "retweet"
|
||||
REPLY = "reply"
|
||||
QUOTE_TWEET = "quote_tweet"
|
||||
BOOKMARK = "bookmark"
|
||||
IMPRESSION = "impression"
|
||||
PROFILE_CLICK = "profile_click"
|
||||
URL_CLICK = "url_click"
|
||||
HASHTAG_CLICK = "hashtag_click"
|
||||
MENTION_CLICK = "mention_click"
|
||||
|
||||
class AnalyticsTimeframe(enum.Enum):
|
||||
HOURLY = "hourly"
|
||||
DAILY = "daily"
|
||||
WEEKLY = "weekly"
|
||||
MONTHLY = "monthly"
|
||||
|
||||
class ContentCategory(enum.Enum):
|
||||
EDUCATIONAL = "educational"
|
||||
PROMOTIONAL = "promotional"
|
||||
PERSONAL = "personal"
|
||||
NEWS = "news"
|
||||
ENTERTAINMENT = "entertainment"
|
||||
QUESTION = "question"
|
||||
POLL = "poll"
|
||||
THREAD = "thread"
|
||||
|
||||
# --- DATACLASSES ---
|
||||
|
||||
@dataclass
|
||||
class TwitterCredentials:
|
||||
"""Dataclass for Twitter API credentials"""
|
||||
api_key: str = ""
|
||||
api_secret: str = ""
|
||||
access_token: str = ""
|
||||
access_token_secret: str = ""
|
||||
bearer_token: str = ""
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return {
|
||||
'api_key': self.api_key,
|
||||
'api_secret': self.api_secret,
|
||||
'access_token': self.access_token,
|
||||
'access_token_secret': self.access_token_secret,
|
||||
'bearer_token': self.bearer_token
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, str]) -> 'TwitterCredentials':
|
||||
return cls(
|
||||
api_key=data.get('api_key', ''),
|
||||
api_secret=data.get('api_secret', ''),
|
||||
access_token=data.get('access_token', ''),
|
||||
access_token_secret=data.get('access_token_secret', ''),
|
||||
bearer_token=data.get('bearer_token', '')
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class TweetMetrics:
|
||||
"""Dataclass for tweet performance metrics"""
|
||||
likes: int = 0
|
||||
retweets: int = 0
|
||||
replies: int = 0
|
||||
quotes: int = 0
|
||||
bookmarks: int = 0
|
||||
impressions: int = 0
|
||||
profile_clicks: int = 0
|
||||
url_clicks: int = 0
|
||||
hashtag_clicks: int = 0
|
||||
engagement_rate: float = 0.0
|
||||
reach: int = 0
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'likes': self.likes,
|
||||
'retweets': self.retweets,
|
||||
'replies': self.replies,
|
||||
'quotes': self.quotes,
|
||||
'bookmarks': self.bookmarks,
|
||||
'impressions': self.impressions,
|
||||
'profile_clicks': self.profile_clicks,
|
||||
'url_clicks': self.url_clicks,
|
||||
'hashtag_clicks': self.hashtag_clicks,
|
||||
'engagement_rate': self.engagement_rate,
|
||||
'reach': self.reach
|
||||
}
|
||||
|
||||
# --- MODELS ---
|
||||
|
||||
class TwitterUser(Base):
|
||||
"""
|
||||
Stores Twitter user profile information and authentication data.
|
||||
This reduces API calls for user profile information.
|
||||
"""
|
||||
__tablename__ = "twitter_users"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(String, nullable=False, unique=True) # ALwrity user ID
|
||||
twitter_user_id = Column(BigInteger, nullable=False, unique=True) # Twitter user ID
|
||||
username = Column(String, nullable=False, index=True) # @username
|
||||
display_name = Column(String, nullable=False)
|
||||
bio = Column(Text)
|
||||
location = Column(String)
|
||||
website = Column(String)
|
||||
profile_image_url = Column(String)
|
||||
banner_image_url = Column(String)
|
||||
|
||||
# Account metrics
|
||||
followers_count = Column(Integer, default=0)
|
||||
following_count = Column(Integer, default=0)
|
||||
tweet_count = Column(Integer, default=0)
|
||||
listed_count = Column(Integer, default=0)
|
||||
|
||||
# Account details
|
||||
account_type = Column(Enum(TwitterAccountType), default=TwitterAccountType.PERSONAL)
|
||||
verified = Column(Boolean, default=False)
|
||||
protected = Column(Boolean, default=False)
|
||||
created_at_twitter = Column(DateTime) # When Twitter account was created
|
||||
|
||||
# Authentication and API data
|
||||
credentials_encrypted = Column(Text) # Encrypted JSON of TwitterCredentials
|
||||
api_rate_limit_remaining = Column(Integer, default=0)
|
||||
api_rate_limit_reset = Column(DateTime)
|
||||
last_api_call = Column(DateTime)
|
||||
|
||||
# Metadata
|
||||
is_active = Column(Boolean, default=True)
|
||||
last_sync = Column(DateTime, default=datetime.utcnow)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
tweets = relationship("Tweet", back_populates="user", cascade="all, delete-orphan")
|
||||
analytics = relationship("TwitterAnalytics", back_populates="user", cascade="all, delete-orphan")
|
||||
scheduled_tweets = relationship("ScheduledTweet", back_populates="user", cascade="all, delete-orphan")
|
||||
engagement_data = relationship("EngagementData", back_populates="user", cascade="all, delete-orphan")
|
||||
audience_insights = relationship("AudienceInsight", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index('idx_twitter_user_username', 'username'),
|
||||
Index('idx_twitter_user_sync', 'last_sync'),
|
||||
Index('idx_twitter_user_active', 'is_active'),
|
||||
)
|
||||
|
||||
class Tweet(Base):
|
||||
"""
|
||||
Stores tweet content, metadata, and performance data.
|
||||
Includes both posted tweets and drafts.
|
||||
"""
|
||||
__tablename__ = "tweets"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("twitter_users.id"), nullable=False)
|
||||
tweet_id = Column(BigInteger, unique=True, index=True) # Twitter tweet ID (null for drafts)
|
||||
|
||||
# Content
|
||||
text = Column(Text, nullable=False)
|
||||
hashtags = Column(JSON, default=list) # List of hashtags
|
||||
mentions = Column(JSON, default=list) # List of mentioned users
|
||||
urls = Column(JSON, default=list) # List of URLs in tweet
|
||||
media_urls = Column(JSON, default=list) # List of media URLs
|
||||
|
||||
# Tweet metadata
|
||||
tweet_type = Column(Enum(TweetType), default=TweetType.ORIGINAL)
|
||||
status = Column(Enum(TweetStatus), default=TweetStatus.DRAFT)
|
||||
category = Column(Enum(ContentCategory))
|
||||
|
||||
# Engagement metrics (updated periodically)
|
||||
likes_count = Column(Integer, default=0)
|
||||
retweets_count = Column(Integer, default=0)
|
||||
replies_count = Column(Integer, default=0)
|
||||
quotes_count = Column(Integer, default=0)
|
||||
bookmarks_count = Column(Integer, default=0)
|
||||
impressions_count = Column(Integer, default=0)
|
||||
|
||||
# Performance metrics
|
||||
engagement_rate = Column(Float, default=0.0)
|
||||
reach = Column(Integer, default=0)
|
||||
click_through_rate = Column(Float, default=0.0)
|
||||
|
||||
# AI and generation data
|
||||
ai_generated = Column(Boolean, default=False)
|
||||
ai_model_used = Column(String) # Which AI model generated this
|
||||
ai_prompt = Column(Text) # Original prompt used
|
||||
ai_confidence_score = Column(Float) # AI confidence in content quality
|
||||
generation_metadata = Column(JSON, default=dict) # Additional AI metadata
|
||||
|
||||
# Scheduling and posting
|
||||
scheduled_for = Column(DateTime)
|
||||
posted_at = Column(DateTime)
|
||||
last_metrics_update = Column(DateTime)
|
||||
|
||||
# Thread information
|
||||
thread_id = Column(String) # For grouping thread tweets
|
||||
thread_position = Column(Integer) # Position in thread (1, 2, 3...)
|
||||
parent_tweet_id = Column(BigInteger) # For replies
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
user = relationship("TwitterUser", back_populates="tweets")
|
||||
analytics = relationship("TweetAnalytics", back_populates="tweet", cascade="all, delete-orphan")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index('idx_tweet_user_status', 'user_id', 'status'),
|
||||
Index('idx_tweet_posted_at', 'posted_at'),
|
||||
Index('idx_tweet_engagement', 'engagement_rate'),
|
||||
Index('idx_tweet_thread', 'thread_id'),
|
||||
)
|
||||
|
||||
class ScheduledTweet(Base):
|
||||
"""
|
||||
Stores scheduled tweets with automation settings.
|
||||
"""
|
||||
__tablename__ = "scheduled_tweets"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("twitter_users.id"), nullable=False)
|
||||
tweet_id = Column(Integer, ForeignKey("tweets.id"), nullable=False)
|
||||
|
||||
# Scheduling details
|
||||
scheduled_time = Column(DateTime, nullable=False)
|
||||
timezone = Column(String, default="UTC")
|
||||
recurrence_pattern = Column(String) # cron-like pattern for recurring tweets
|
||||
|
||||
# Automation settings
|
||||
auto_optimize_time = Column(Boolean, default=False) # AI-optimize posting time
|
||||
auto_add_hashtags = Column(Boolean, default=False)
|
||||
auto_add_emojis = Column(Boolean, default=False)
|
||||
|
||||
# Status and execution
|
||||
status = Column(Enum(TweetStatus), default=TweetStatus.SCHEDULED)
|
||||
attempts = Column(Integer, default=0)
|
||||
last_attempt = Column(DateTime)
|
||||
error_message = Column(Text)
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
user = relationship("TwitterUser", back_populates="scheduled_tweets")
|
||||
tweet = relationship("Tweet")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index('idx_scheduled_time', 'scheduled_time'),
|
||||
Index('idx_scheduled_status', 'status'),
|
||||
)
|
||||
|
||||
class TwitterAnalytics(Base):
|
||||
"""
|
||||
Stores aggregated Twitter analytics data for users.
|
||||
Updated periodically to track account performance over time.
|
||||
"""
|
||||
__tablename__ = "twitter_analytics"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("twitter_users.id"), nullable=False)
|
||||
|
||||
# Time period
|
||||
date = Column(DateTime, nullable=False)
|
||||
timeframe = Column(Enum(AnalyticsTimeframe), nullable=False)
|
||||
|
||||
# Account metrics
|
||||
followers_gained = Column(Integer, default=0)
|
||||
followers_lost = Column(Integer, default=0)
|
||||
net_follower_change = Column(Integer, default=0)
|
||||
following_change = Column(Integer, default=0)
|
||||
|
||||
# Content metrics
|
||||
tweets_posted = Column(Integer, default=0)
|
||||
total_impressions = Column(Integer, default=0)
|
||||
total_engagements = Column(Integer, default=0)
|
||||
total_likes = Column(Integer, default=0)
|
||||
total_retweets = Column(Integer, default=0)
|
||||
total_replies = Column(Integer, default=0)
|
||||
total_quotes = Column(Integer, default=0)
|
||||
|
||||
# Performance metrics
|
||||
average_engagement_rate = Column(Float, default=0.0)
|
||||
top_tweet_id = Column(BigInteger) # Best performing tweet
|
||||
top_tweet_engagement = Column(Integer, default=0)
|
||||
|
||||
# Audience metrics
|
||||
profile_visits = Column(Integer, default=0)
|
||||
mention_count = Column(Integer, default=0)
|
||||
hashtag_performance = Column(JSON, default=dict) # Top hashtags and their performance
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
user = relationship("TwitterUser", back_populates="analytics")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index('idx_analytics_user_date', 'user_id', 'date'),
|
||||
Index('idx_analytics_timeframe', 'timeframe'),
|
||||
UniqueConstraint('user_id', 'date', 'timeframe', name='uq_user_date_timeframe'),
|
||||
)
|
||||
|
||||
class TweetAnalytics(Base):
|
||||
"""
|
||||
Stores detailed analytics for individual tweets.
|
||||
Updated periodically to track tweet performance over time.
|
||||
"""
|
||||
__tablename__ = "tweet_analytics"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
tweet_id = Column(Integer, ForeignKey("tweets.id"), nullable=False)
|
||||
|
||||
# Time period
|
||||
recorded_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
# Engagement metrics
|
||||
likes = Column(Integer, default=0)
|
||||
retweets = Column(Integer, default=0)
|
||||
replies = Column(Integer, default=0)
|
||||
quotes = Column(Integer, default=0)
|
||||
bookmarks = Column(Integer, default=0)
|
||||
|
||||
# Reach metrics
|
||||
impressions = Column(Integer, default=0)
|
||||
reach = Column(Integer, default=0)
|
||||
profile_clicks = Column(Integer, default=0)
|
||||
|
||||
# Click metrics
|
||||
url_clicks = Column(Integer, default=0)
|
||||
hashtag_clicks = Column(Integer, default=0)
|
||||
mention_clicks = Column(Integer, default=0)
|
||||
media_views = Column(Integer, default=0)
|
||||
|
||||
# Calculated metrics
|
||||
engagement_rate = Column(Float, default=0.0)
|
||||
click_through_rate = Column(Float, default=0.0)
|
||||
virality_score = Column(Float, default=0.0) # Custom metric for viral potential
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
tweet = relationship("Tweet", back_populates="analytics")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index('idx_tweet_analytics_recorded', 'recorded_at'),
|
||||
Index('idx_tweet_analytics_engagement', 'engagement_rate'),
|
||||
)
|
||||
|
||||
class EngagementData(Base):
|
||||
"""
|
||||
Stores individual engagement events for detailed analysis.
|
||||
"""
|
||||
__tablename__ = "engagement_data"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("twitter_users.id"), nullable=False)
|
||||
tweet_id = Column(Integer, ForeignKey("tweets.id"))
|
||||
|
||||
# Engagement details
|
||||
engagement_type = Column(Enum(EngagementType), nullable=False)
|
||||
engaging_user_id = Column(BigInteger) # Twitter ID of user who engaged
|
||||
engaging_username = Column(String)
|
||||
|
||||
# Metadata
|
||||
occurred_at = Column(DateTime, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
user = relationship("TwitterUser", back_populates="engagement_data")
|
||||
tweet = relationship("Tweet")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index('idx_engagement_user_type', 'user_id', 'engagement_type'),
|
||||
Index('idx_engagement_occurred', 'occurred_at'),
|
||||
)
|
||||
|
||||
class AudienceInsight(Base):
|
||||
"""
|
||||
Stores audience demographics and behavior insights.
|
||||
"""
|
||||
__tablename__ = "audience_insights"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("twitter_users.id"), nullable=False)
|
||||
|
||||
# Time period
|
||||
date = Column(DateTime, nullable=False)
|
||||
|
||||
# Demographics (aggregated data)
|
||||
top_locations = Column(JSON, default=list) # Top follower locations
|
||||
age_demographics = Column(JSON, default=dict) # Age distribution
|
||||
gender_demographics = Column(JSON, default=dict) # Gender distribution
|
||||
language_demographics = Column(JSON, default=dict) # Language distribution
|
||||
|
||||
# Behavior insights
|
||||
most_active_hours = Column(JSON, default=list) # When audience is most active
|
||||
top_interests = Column(JSON, default=list) # Audience interests
|
||||
engagement_patterns = Column(JSON, default=dict) # How audience engages
|
||||
|
||||
# Content preferences
|
||||
preferred_content_types = Column(JSON, default=dict)
|
||||
top_hashtags_used = Column(JSON, default=list)
|
||||
response_rate_by_content = Column(JSON, default=dict)
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
user = relationship("TwitterUser", back_populates="audience_insights")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index('idx_audience_user_date', 'user_id', 'date'),
|
||||
)
|
||||
|
||||
class HashtagPerformance(Base):
|
||||
"""
|
||||
Tracks performance of hashtags used by the user.
|
||||
"""
|
||||
__tablename__ = "hashtag_performance"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("twitter_users.id"), nullable=False)
|
||||
|
||||
# Hashtag details
|
||||
hashtag = Column(String, nullable=False, index=True)
|
||||
usage_count = Column(Integer, default=0)
|
||||
|
||||
# Performance metrics
|
||||
total_impressions = Column(Integer, default=0)
|
||||
total_engagements = Column(Integer, default=0)
|
||||
average_engagement_rate = Column(Float, default=0.0)
|
||||
|
||||
# Best performing tweet with this hashtag
|
||||
best_tweet_id = Column(Integer, ForeignKey("tweets.id"))
|
||||
best_tweet_engagement = Column(Integer, default=0)
|
||||
|
||||
# Time tracking
|
||||
first_used = Column(DateTime)
|
||||
last_used = Column(DateTime)
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
user = relationship("TwitterUser")
|
||||
best_tweet = relationship("Tweet")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index('idx_hashtag_user_performance', 'user_id', 'average_engagement_rate'),
|
||||
UniqueConstraint('user_id', 'hashtag', name='uq_user_hashtag'),
|
||||
)
|
||||
|
||||
class ContentTemplate(Base):
|
||||
"""
|
||||
Stores reusable tweet templates and AI prompts.
|
||||
"""
|
||||
__tablename__ = "content_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("twitter_users.id"), nullable=False)
|
||||
|
||||
# Template details
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(Text)
|
||||
template_text = Column(Text, nullable=False)
|
||||
category = Column(Enum(ContentCategory))
|
||||
|
||||
# Template variables and settings
|
||||
variables = Column(JSON, default=list) # List of template variables
|
||||
default_hashtags = Column(JSON, default=list)
|
||||
suggested_times = Column(JSON, default=list) # Best times to post this type
|
||||
|
||||
# AI settings
|
||||
ai_prompt = Column(Text) # AI prompt for generating content
|
||||
ai_tone = Column(String) # Tone for AI generation
|
||||
ai_target_audience = Column(String)
|
||||
|
||||
# Usage tracking
|
||||
usage_count = Column(Integer, default=0)
|
||||
last_used = Column(DateTime)
|
||||
average_performance = Column(Float, default=0.0)
|
||||
|
||||
# Metadata
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
user = relationship("TwitterUser")
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
Index('idx_template_user_category', 'user_id', 'category'),
|
||||
Index('idx_template_performance', 'average_performance'),
|
||||
)
|
||||
|
||||
class TwitterSettings(Base):
|
||||
"""
|
||||
Stores user-specific Twitter settings and preferences.
|
||||
"""
|
||||
__tablename__ = "twitter_settings"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("twitter_users.id"), nullable=False, unique=True)
|
||||
|
||||
# Posting preferences
|
||||
default_hashtags = Column(JSON, default=list)
|
||||
auto_add_hashtags = Column(Boolean, default=False)
|
||||
auto_add_emojis = Column(Boolean, default=False)
|
||||
max_hashtags_per_tweet = Column(Integer, default=2)
|
||||
|
||||
# Scheduling preferences
|
||||
preferred_posting_times = Column(JSON, default=list)
|
||||
timezone = Column(String, default="UTC")
|
||||
auto_optimize_timing = Column(Boolean, default=False)
|
||||
|
||||
# AI preferences
|
||||
ai_tone_preference = Column(String, default="casual")
|
||||
ai_target_audience = Column(String, default="general")
|
||||
ai_creativity_level = Column(Float, default=0.7) # 0-1 scale
|
||||
|
||||
# Analytics preferences
|
||||
analytics_frequency = Column(String, default="daily") # hourly, daily, weekly
|
||||
track_competitor_hashtags = Column(JSON, default=list)
|
||||
notification_preferences = Column(JSON, default=dict)
|
||||
|
||||
# Content preferences
|
||||
content_categories = Column(JSON, default=list) # Preferred content types
|
||||
avoid_topics = Column(JSON, default=list) # Topics to avoid
|
||||
brand_keywords = Column(JSON, default=list) # Brand-related keywords
|
||||
|
||||
# Automation settings
|
||||
auto_retweet_keywords = Column(JSON, default=list)
|
||||
auto_like_keywords = Column(JSON, default=list)
|
||||
auto_follow_back = Column(Boolean, default=False)
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
user = relationship("TwitterUser")
|
||||
|
||||
# --- DATABASE FUNCTIONS ---
|
||||
|
||||
def get_twitter_engine(db_url: str = "sqlite:///twitter_data.db"):
|
||||
"""Create and return database engine for Twitter data."""
|
||||
return create_engine(db_url, echo=False)
|
||||
|
||||
def init_twitter_db(engine):
|
||||
"""Initialize Twitter database tables."""
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
def get_twitter_session(engine):
|
||||
"""Create and return database session for Twitter data."""
|
||||
Session = sessionmaker(bind=engine)
|
||||
return Session()
|
||||
|
||||
def create_twitter_user(session, user_data: Dict[str, Any]) -> TwitterUser:
|
||||
"""Create a new Twitter user record."""
|
||||
twitter_user = TwitterUser(
|
||||
user_id=user_data['user_id'],
|
||||
twitter_user_id=user_data['twitter_user_id'],
|
||||
username=user_data['username'],
|
||||
display_name=user_data['display_name'],
|
||||
bio=user_data.get('bio', ''),
|
||||
location=user_data.get('location', ''),
|
||||
website=user_data.get('website', ''),
|
||||
profile_image_url=user_data.get('profile_image_url', ''),
|
||||
banner_image_url=user_data.get('banner_image_url', ''),
|
||||
followers_count=user_data.get('followers_count', 0),
|
||||
following_count=user_data.get('following_count', 0),
|
||||
tweet_count=user_data.get('tweet_count', 0),
|
||||
verified=user_data.get('verified', False),
|
||||
protected=user_data.get('protected', False),
|
||||
created_at_twitter=user_data.get('created_at_twitter'),
|
||||
credentials_encrypted=user_data.get('credentials_encrypted', ''),
|
||||
)
|
||||
|
||||
session.add(twitter_user)
|
||||
session.commit()
|
||||
return twitter_user
|
||||
|
||||
def update_user_metrics(session, user_id: int, metrics: Dict[str, Any]):
|
||||
"""Update user metrics from Twitter API."""
|
||||
user = session.query(TwitterUser).filter_by(id=user_id).first()
|
||||
if user:
|
||||
user.followers_count = metrics.get('followers_count', user.followers_count)
|
||||
user.following_count = metrics.get('following_count', user.following_count)
|
||||
user.tweet_count = metrics.get('tweet_count', user.tweet_count)
|
||||
user.last_sync = datetime.utcnow()
|
||||
session.commit()
|
||||
|
||||
def create_tweet_record(session, tweet_data: Dict[str, Any]) -> Tweet:
|
||||
"""Create a new tweet record."""
|
||||
# Handle both 'text' and 'content' field names for compatibility
|
||||
text_content = tweet_data.get('text') or tweet_data.get('content')
|
||||
if not text_content:
|
||||
raise ValueError("Tweet must have either 'text' or 'content' field")
|
||||
|
||||
tweet = Tweet(
|
||||
user_id=tweet_data['user_id'],
|
||||
tweet_id=tweet_data.get('tweet_id'),
|
||||
text=text_content,
|
||||
hashtags=tweet_data.get('hashtags', []),
|
||||
mentions=tweet_data.get('mentions', []),
|
||||
urls=tweet_data.get('urls', []),
|
||||
media_urls=tweet_data.get('media_urls', []),
|
||||
tweet_type=TweetType(tweet_data.get('tweet_type', 'original')),
|
||||
status=TweetStatus(tweet_data.get('status', 'draft')),
|
||||
category=ContentCategory(tweet_data['category']) if tweet_data.get('category') else None,
|
||||
ai_generated=tweet_data.get('ai_generated', False),
|
||||
ai_model_used=tweet_data.get('ai_model_used'),
|
||||
ai_prompt=tweet_data.get('ai_prompt'),
|
||||
ai_confidence_score=tweet_data.get('ai_confidence_score'),
|
||||
generation_metadata=tweet_data.get('generation_metadata', {}),
|
||||
scheduled_for=tweet_data.get('scheduled_for'),
|
||||
posted_at=tweet_data.get('posted_at'),
|
||||
thread_id=tweet_data.get('thread_id'),
|
||||
thread_position=tweet_data.get('thread_position'),
|
||||
parent_tweet_id=tweet_data.get('parent_tweet_id'),
|
||||
)
|
||||
|
||||
session.add(tweet)
|
||||
session.commit()
|
||||
return tweet
|
||||
|
||||
def update_tweet_metrics(session, tweet_id: int, metrics: TweetMetrics):
|
||||
"""Update tweet metrics from Twitter API."""
|
||||
tweet = session.query(Tweet).filter_by(id=tweet_id).first()
|
||||
if tweet:
|
||||
tweet.likes_count = metrics.likes
|
||||
tweet.retweets_count = metrics.retweets
|
||||
tweet.replies_count = metrics.replies
|
||||
tweet.quotes_count = metrics.quotes
|
||||
tweet.bookmarks_count = metrics.bookmarks
|
||||
tweet.impressions_count = metrics.impressions
|
||||
tweet.engagement_rate = metrics.engagement_rate
|
||||
tweet.reach = metrics.reach
|
||||
tweet.last_metrics_update = datetime.utcnow()
|
||||
session.commit()
|
||||
|
||||
# Also create analytics record
|
||||
analytics = TweetAnalytics(
|
||||
tweet_id=tweet_id,
|
||||
likes=metrics.likes,
|
||||
retweets=metrics.retweets,
|
||||
replies=metrics.replies,
|
||||
quotes=metrics.quotes,
|
||||
bookmarks=metrics.bookmarks,
|
||||
impressions=metrics.impressions,
|
||||
reach=metrics.reach,
|
||||
engagement_rate=metrics.engagement_rate,
|
||||
click_through_rate=metrics.url_clicks / max(metrics.impressions, 1) * 100,
|
||||
virality_score=calculate_virality_score(metrics)
|
||||
)
|
||||
session.add(analytics)
|
||||
session.commit()
|
||||
|
||||
def calculate_virality_score(metrics: TweetMetrics) -> float:
|
||||
"""Calculate a custom virality score based on engagement metrics."""
|
||||
if metrics.impressions == 0:
|
||||
return 0.0
|
||||
|
||||
# Weight different engagement types
|
||||
engagement_score = (
|
||||
metrics.likes * 1.0 +
|
||||
metrics.retweets * 3.0 + # Retweets are more valuable
|
||||
metrics.replies * 2.0 +
|
||||
metrics.quotes * 2.5 +
|
||||
metrics.bookmarks * 1.5
|
||||
)
|
||||
|
||||
# Normalize by impressions and scale
|
||||
virality = (engagement_score / metrics.impressions) * 100
|
||||
return min(virality, 100.0) # Cap at 100
|
||||
|
||||
def get_user_analytics_summary(session, user_id: int, days: int = 30) -> Dict[str, Any]:
|
||||
"""Get analytics summary for a user over specified days."""
|
||||
from sqlalchemy import func
|
||||
|
||||
start_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
# Get tweet metrics
|
||||
tweet_stats = session.query(
|
||||
func.count(Tweet.id).label('total_tweets'),
|
||||
func.avg(Tweet.engagement_rate).label('avg_engagement'),
|
||||
func.sum(Tweet.likes_count).label('total_likes'),
|
||||
func.sum(Tweet.retweets_count).label('total_retweets'),
|
||||
func.sum(Tweet.impressions_count).label('total_impressions')
|
||||
).filter(
|
||||
Tweet.user_id == user_id,
|
||||
Tweet.posted_at >= start_date,
|
||||
Tweet.status == TweetStatus.POSTED
|
||||
).first()
|
||||
|
||||
# Get follower growth
|
||||
user = session.query(TwitterUser).filter_by(id=user_id).first()
|
||||
|
||||
return {
|
||||
'total_tweets': tweet_stats.total_tweets or 0,
|
||||
'average_engagement_rate': float(tweet_stats.avg_engagement or 0),
|
||||
'total_likes': tweet_stats.total_likes or 0,
|
||||
'total_retweets': tweet_stats.total_retweets or 0,
|
||||
'total_impressions': tweet_stats.total_impressions or 0,
|
||||
'current_followers': user.followers_count if user else 0,
|
||||
'period_days': days
|
||||
}
|
||||
|
||||
# Export all models and functions
|
||||
__all__ = [
|
||||
# Models
|
||||
'TwitterUser', 'Tweet', 'ScheduledTweet', 'TwitterAnalytics', 'TweetAnalytics',
|
||||
'EngagementData', 'AudienceInsight', 'HashtagPerformance', 'ContentTemplate',
|
||||
'TwitterSettings',
|
||||
|
||||
# Enums
|
||||
'TwitterAccountType', 'TweetType', 'TweetStatus', 'EngagementType',
|
||||
'AnalyticsTimeframe', 'ContentCategory',
|
||||
|
||||
# Dataclasses
|
||||
'TwitterCredentials', 'TweetMetrics',
|
||||
|
||||
# Functions
|
||||
'get_twitter_engine', 'init_twitter_db', 'get_twitter_session',
|
||||
'create_twitter_user', 'update_user_metrics', 'create_tweet_record',
|
||||
'update_tweet_metrics', 'calculate_virality_score', 'get_user_analytics_summary'
|
||||
]
|
||||
@@ -1,766 +0,0 @@
|
||||
"""
|
||||
Twitter Database Service Layer
|
||||
=============================
|
||||
|
||||
This module provides high-level service functions for managing Twitter data
|
||||
in the database. It acts as an interface between the application and the
|
||||
database models, providing convenient methods for common operations.
|
||||
|
||||
Key Features:
|
||||
- User profile management and synchronization
|
||||
- Tweet creation, updating, and analytics tracking
|
||||
- Scheduled tweet management
|
||||
- Analytics data aggregation and reporting
|
||||
- Hashtag performance tracking
|
||||
- Audience insights management
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, desc, and_, or_
|
||||
import json
|
||||
from cryptography.fernet import Fernet
|
||||
import os
|
||||
|
||||
from .twitter_models import (
|
||||
TwitterUser, Tweet, ScheduledTweet, TwitterAnalytics, TweetAnalytics,
|
||||
EngagementData, AudienceInsight, HashtagPerformance, ContentTemplate,
|
||||
TwitterSettings, TwitterCredentials, TweetMetrics,
|
||||
TwitterAccountType, TweetType, TweetStatus, EngagementType,
|
||||
AnalyticsTimeframe, ContentCategory,
|
||||
get_twitter_engine, init_twitter_db, get_twitter_session,
|
||||
create_twitter_user, update_user_metrics, create_tweet_record,
|
||||
update_tweet_metrics, calculate_virality_score, get_user_analytics_summary
|
||||
)
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TwitterDatabaseService:
|
||||
"""
|
||||
High-level service for managing Twitter data in the database.
|
||||
"""
|
||||
|
||||
def __init__(self, db_url: str = "sqlite:///twitter_data.db", encryption_key: Optional[str] = None):
|
||||
"""Initialize the Twitter database service."""
|
||||
self.engine = get_twitter_engine(db_url)
|
||||
self.encryption_key = encryption_key or self._get_or_create_encryption_key()
|
||||
self.cipher = Fernet(self.encryption_key.encode() if isinstance(self.encryption_key, str) else self.encryption_key)
|
||||
|
||||
# Initialize database
|
||||
init_twitter_db(self.engine)
|
||||
|
||||
logger.info("Twitter database service initialized")
|
||||
|
||||
def _get_or_create_encryption_key(self) -> str:
|
||||
"""Get or create encryption key for sensitive data."""
|
||||
key_file = "twitter_encryption.key"
|
||||
|
||||
if os.path.exists(key_file):
|
||||
with open(key_file, 'rb') as f:
|
||||
return f.read()
|
||||
else:
|
||||
key = Fernet.generate_key()
|
||||
with open(key_file, 'wb') as f:
|
||||
f.write(key)
|
||||
return key
|
||||
|
||||
def _encrypt_credentials(self, credentials: TwitterCredentials) -> str:
|
||||
"""Encrypt Twitter credentials for secure storage."""
|
||||
credentials_json = json.dumps(credentials.to_dict())
|
||||
encrypted = self.cipher.encrypt(credentials_json.encode())
|
||||
return encrypted.decode()
|
||||
|
||||
def _decrypt_credentials(self, encrypted_credentials: str) -> TwitterCredentials:
|
||||
"""Decrypt Twitter credentials from storage."""
|
||||
try:
|
||||
decrypted = self.cipher.decrypt(encrypted_credentials.encode())
|
||||
credentials_dict = json.loads(decrypted.decode())
|
||||
return TwitterCredentials.from_dict(credentials_dict)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decrypt credentials: {e}")
|
||||
return TwitterCredentials()
|
||||
|
||||
def get_session(self) -> Session:
|
||||
"""Get a database session."""
|
||||
return get_twitter_session(self.engine)
|
||||
|
||||
# --- USER MANAGEMENT ---
|
||||
|
||||
def create_or_update_user(self, user_data: Dict[str, Any]) -> TwitterUser:
|
||||
"""Create a new Twitter user or update existing one."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
# Check if user already exists
|
||||
existing_user = session.query(TwitterUser).filter_by(
|
||||
user_id=user_data['user_id']
|
||||
).first()
|
||||
|
||||
if existing_user:
|
||||
# Update existing user
|
||||
for key, value in user_data.items():
|
||||
if hasattr(existing_user, key) and key != 'id':
|
||||
setattr(existing_user, key, value)
|
||||
existing_user.updated_at = datetime.utcnow()
|
||||
session.commit()
|
||||
logger.info(f"Updated Twitter user: {existing_user.username}")
|
||||
return existing_user
|
||||
else:
|
||||
# Create new user
|
||||
twitter_user = create_twitter_user(session, user_data)
|
||||
logger.info(f"Created new Twitter user: {twitter_user.username}")
|
||||
return twitter_user
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error creating/updating user: {e}")
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def save_user_credentials(self, user_id: str, credentials: TwitterCredentials) -> bool:
|
||||
"""Save encrypted Twitter credentials for a user."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if user:
|
||||
encrypted_creds = self._encrypt_credentials(credentials)
|
||||
user.credentials_encrypted = encrypted_creds
|
||||
user.updated_at = datetime.utcnow()
|
||||
session.commit()
|
||||
logger.info(f"Saved credentials for user: {user.username}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"User not found: {user_id}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error saving credentials: {e}")
|
||||
return False
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_user_credentials(self, user_id: str) -> Optional[TwitterCredentials]:
|
||||
"""Get decrypted Twitter credentials for a user."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if user and user.credentials_encrypted:
|
||||
return self._decrypt_credentials(user.credentials_encrypted)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting credentials: {e}")
|
||||
return None
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_user_by_id(self, user_id: str) -> Optional[TwitterUser]:
|
||||
"""Get Twitter user by ALwrity user ID."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
return session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_user_by_twitter_id(self, twitter_user_id: int) -> Optional[TwitterUser]:
|
||||
"""Get Twitter user by Twitter user ID."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
return session.query(TwitterUser).filter_by(twitter_user_id=twitter_user_id).first()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def update_user_profile(self, user_id: str, profile_data: Dict[str, Any]) -> bool:
|
||||
"""Update user profile information from Twitter API."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if user:
|
||||
update_user_metrics(session, user.id, profile_data)
|
||||
logger.info(f"Updated profile for user: {user.username}")
|
||||
return True
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error updating user profile: {e}")
|
||||
return False
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# --- TWEET MANAGEMENT ---
|
||||
|
||||
def save_tweet(self, tweet_data: Dict[str, Any]) -> Tweet:
|
||||
"""Save a tweet to the database."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
tweet = create_tweet_record(session, tweet_data)
|
||||
logger.info(f"Saved tweet: {tweet.id}")
|
||||
return tweet
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error saving tweet: {e}")
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def update_tweet_status(self, tweet_id: int, status: TweetStatus, twitter_tweet_id: Optional[int] = None) -> bool:
|
||||
"""Update tweet status (e.g., from draft to posted)."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
tweet = session.query(Tweet).filter_by(id=tweet_id).first()
|
||||
if tweet:
|
||||
tweet.status = status
|
||||
if twitter_tweet_id:
|
||||
tweet.tweet_id = twitter_tweet_id
|
||||
if status == TweetStatus.POSTED:
|
||||
tweet.posted_at = datetime.utcnow()
|
||||
tweet.updated_at = datetime.utcnow()
|
||||
session.commit()
|
||||
logger.info(f"Updated tweet {tweet_id} status to {status.value}")
|
||||
return True
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error updating tweet status: {e}")
|
||||
return False
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_user_tweets(self, user_id: str, status: Optional[TweetStatus] = None, limit: int = 50) -> List[Tweet]:
|
||||
"""Get tweets for a user, optionally filtered by status."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if not user:
|
||||
return []
|
||||
|
||||
query = session.query(Tweet).filter_by(user_id=user.id)
|
||||
|
||||
if status:
|
||||
query = query.filter_by(status=status)
|
||||
|
||||
return query.order_by(desc(Tweet.created_at)).limit(limit).all()
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_tweet_by_id(self, tweet_id: int) -> Optional[Tweet]:
|
||||
"""Get tweet by database ID."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
return session.query(Tweet).filter_by(id=tweet_id).first()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_tweet_by_twitter_id(self, twitter_tweet_id: int) -> Optional[Tweet]:
|
||||
"""Get tweet by Twitter tweet ID."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
return session.query(Tweet).filter_by(tweet_id=twitter_tweet_id).first()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def update_tweet_analytics(self, tweet_id: int, metrics: TweetMetrics) -> bool:
|
||||
"""Update tweet analytics from Twitter API."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
update_tweet_metrics(session, tweet_id, metrics)
|
||||
logger.info(f"Updated analytics for tweet: {tweet_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error updating tweet analytics: {e}")
|
||||
return False
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_top_performing_tweets(self, user_id: str, days: int = 30, limit: int = 10) -> List[Tweet]:
|
||||
"""Get top performing tweets for a user."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if not user:
|
||||
return []
|
||||
|
||||
start_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
return session.query(Tweet).filter(
|
||||
and_(
|
||||
Tweet.user_id == user.id,
|
||||
Tweet.status == TweetStatus.POSTED,
|
||||
Tweet.posted_at >= start_date
|
||||
)
|
||||
).order_by(desc(Tweet.engagement_rate)).limit(limit).all()
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# --- SCHEDULED TWEETS ---
|
||||
|
||||
def schedule_tweet(self, tweet_id: int, scheduled_time: datetime, settings: Dict[str, Any] = None) -> ScheduledTweet:
|
||||
"""Schedule a tweet for posting."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
tweet = session.query(Tweet).filter_by(id=tweet_id).first()
|
||||
if not tweet:
|
||||
raise ValueError(f"Tweet {tweet_id} not found")
|
||||
|
||||
scheduled_tweet = ScheduledTweet(
|
||||
user_id=tweet.user_id,
|
||||
tweet_id=tweet_id,
|
||||
scheduled_time=scheduled_time,
|
||||
timezone=settings.get('timezone', 'UTC'),
|
||||
auto_optimize_time=settings.get('auto_optimize_time', False),
|
||||
auto_add_hashtags=settings.get('auto_add_hashtags', False),
|
||||
auto_add_emojis=settings.get('auto_add_emojis', False)
|
||||
)
|
||||
|
||||
session.add(scheduled_tweet)
|
||||
|
||||
# Update tweet status
|
||||
tweet.status = TweetStatus.SCHEDULED
|
||||
tweet.scheduled_for = scheduled_time
|
||||
|
||||
session.commit()
|
||||
logger.info(f"Scheduled tweet {tweet_id} for {scheduled_time}")
|
||||
return scheduled_tweet
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error scheduling tweet: {e}")
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_pending_scheduled_tweets(self, user_id: Optional[str] = None) -> List[ScheduledTweet]:
|
||||
"""Get tweets scheduled for posting."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
query = session.query(ScheduledTweet).filter(
|
||||
and_(
|
||||
ScheduledTweet.status == TweetStatus.SCHEDULED,
|
||||
ScheduledTweet.scheduled_time <= datetime.utcnow()
|
||||
)
|
||||
)
|
||||
|
||||
if user_id:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if user:
|
||||
query = query.filter_by(user_id=user.id)
|
||||
|
||||
return query.order_by(ScheduledTweet.scheduled_time).all()
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def mark_scheduled_tweet_posted(self, scheduled_tweet_id: int, twitter_tweet_id: int) -> bool:
|
||||
"""Mark a scheduled tweet as posted."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
scheduled_tweet = session.query(ScheduledTweet).filter_by(id=scheduled_tweet_id).first()
|
||||
if scheduled_tweet:
|
||||
scheduled_tweet.status = TweetStatus.POSTED
|
||||
|
||||
# Update the associated tweet
|
||||
tweet = session.query(Tweet).filter_by(id=scheduled_tweet.tweet_id).first()
|
||||
if tweet:
|
||||
tweet.status = TweetStatus.POSTED
|
||||
tweet.tweet_id = twitter_tweet_id
|
||||
tweet.posted_at = datetime.utcnow()
|
||||
|
||||
session.commit()
|
||||
logger.info(f"Marked scheduled tweet {scheduled_tweet_id} as posted")
|
||||
return True
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error marking scheduled tweet as posted: {e}")
|
||||
return False
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# --- ANALYTICS ---
|
||||
|
||||
def save_daily_analytics(self, user_id: str, analytics_data: Dict[str, Any]) -> TwitterAnalytics:
|
||||
"""Save daily analytics data for a user."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if not user:
|
||||
raise ValueError(f"User {user_id} not found")
|
||||
|
||||
# Check if analytics for today already exist
|
||||
today = datetime.utcnow().date()
|
||||
existing = session.query(TwitterAnalytics).filter(
|
||||
and_(
|
||||
TwitterAnalytics.user_id == user.id,
|
||||
func.date(TwitterAnalytics.date) == today,
|
||||
TwitterAnalytics.timeframe == AnalyticsTimeframe.DAILY
|
||||
)
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
# Update existing record
|
||||
for key, value in analytics_data.items():
|
||||
if hasattr(existing, key):
|
||||
setattr(existing, key, value)
|
||||
existing.updated_at = datetime.utcnow()
|
||||
session.commit()
|
||||
return existing
|
||||
else:
|
||||
# Create new record
|
||||
analytics = TwitterAnalytics(
|
||||
user_id=user.id,
|
||||
date=datetime.utcnow(),
|
||||
timeframe=AnalyticsTimeframe.DAILY,
|
||||
**analytics_data
|
||||
)
|
||||
session.add(analytics)
|
||||
session.commit()
|
||||
logger.info(f"Saved daily analytics for user: {user.username}")
|
||||
return analytics
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error saving analytics: {e}")
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_analytics_summary(self, user_id: str, days: int = 30) -> Dict[str, Any]:
|
||||
"""Get comprehensive analytics summary for a user."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
return get_user_analytics_summary(session, user_id, days)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_engagement_trends(self, user_id: str, days: int = 30) -> List[Dict[str, Any]]:
|
||||
"""Get engagement trends over time."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if not user:
|
||||
return []
|
||||
|
||||
start_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
analytics = session.query(TwitterAnalytics).filter(
|
||||
and_(
|
||||
TwitterAnalytics.user_id == user.id,
|
||||
TwitterAnalytics.date >= start_date,
|
||||
TwitterAnalytics.timeframe == AnalyticsTimeframe.DAILY
|
||||
)
|
||||
).order_by(TwitterAnalytics.date).all()
|
||||
|
||||
return [
|
||||
{
|
||||
'date': a.date.isoformat(),
|
||||
'engagement_rate': a.average_engagement_rate,
|
||||
'total_engagements': a.total_engagements,
|
||||
'impressions': a.total_impressions,
|
||||
'followers_change': a.net_follower_change
|
||||
}
|
||||
for a in analytics
|
||||
]
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# --- HASHTAG PERFORMANCE ---
|
||||
|
||||
def track_hashtag_performance(self, user_id: str, hashtag: str, tweet_id: int, engagement_metrics: Dict[str, Any]) -> bool:
|
||||
"""Track performance of a hashtag."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if not user:
|
||||
return False
|
||||
|
||||
# Get or create hashtag performance record
|
||||
hashtag_perf = session.query(HashtagPerformance).filter(
|
||||
and_(
|
||||
HashtagPerformance.user_id == user.id,
|
||||
HashtagPerformance.hashtag == hashtag
|
||||
)
|
||||
).first()
|
||||
|
||||
if hashtag_perf:
|
||||
# Update existing record
|
||||
hashtag_perf.usage_count += 1
|
||||
hashtag_perf.total_impressions += engagement_metrics.get('impressions', 0)
|
||||
hashtag_perf.total_engagements += engagement_metrics.get('engagements', 0)
|
||||
hashtag_perf.last_used = datetime.utcnow()
|
||||
|
||||
# Update average engagement rate
|
||||
if hashtag_perf.usage_count > 0:
|
||||
hashtag_perf.average_engagement_rate = (
|
||||
hashtag_perf.total_engagements / hashtag_perf.total_impressions * 100
|
||||
if hashtag_perf.total_impressions > 0 else 0
|
||||
)
|
||||
|
||||
# Update best performing tweet if this one is better
|
||||
current_engagement = engagement_metrics.get('engagements', 0)
|
||||
if current_engagement > hashtag_perf.best_tweet_engagement:
|
||||
hashtag_perf.best_tweet_id = tweet_id
|
||||
hashtag_perf.best_tweet_engagement = current_engagement
|
||||
|
||||
else:
|
||||
# Create new record
|
||||
hashtag_perf = HashtagPerformance(
|
||||
user_id=user.id,
|
||||
hashtag=hashtag,
|
||||
usage_count=1,
|
||||
total_impressions=engagement_metrics.get('impressions', 0),
|
||||
total_engagements=engagement_metrics.get('engagements', 0),
|
||||
average_engagement_rate=(
|
||||
engagement_metrics.get('engagements', 0) /
|
||||
max(engagement_metrics.get('impressions', 1), 1) * 100
|
||||
),
|
||||
best_tweet_id=tweet_id,
|
||||
best_tweet_engagement=engagement_metrics.get('engagements', 0),
|
||||
first_used=datetime.utcnow(),
|
||||
last_used=datetime.utcnow()
|
||||
)
|
||||
session.add(hashtag_perf)
|
||||
|
||||
session.commit()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error tracking hashtag performance: {e}")
|
||||
return False
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_top_hashtags(self, user_id: str, limit: int = 10) -> List[HashtagPerformance]:
|
||||
"""Get top performing hashtags for a user."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if not user:
|
||||
return []
|
||||
|
||||
return session.query(HashtagPerformance).filter_by(
|
||||
user_id=user.id
|
||||
).order_by(desc(HashtagPerformance.average_engagement_rate)).limit(limit).all()
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# --- CONTENT TEMPLATES ---
|
||||
|
||||
def save_content_template(self, user_id: str, template_data: Dict[str, Any]) -> ContentTemplate:
|
||||
"""Save a content template."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if not user:
|
||||
raise ValueError(f"User {user_id} not found")
|
||||
|
||||
template = ContentTemplate(
|
||||
user_id=user.id,
|
||||
name=template_data['name'],
|
||||
description=template_data.get('description', ''),
|
||||
template_text=template_data['template_text'],
|
||||
category=ContentCategory(template_data['category']) if template_data.get('category') else None,
|
||||
variables=template_data.get('variables', []),
|
||||
default_hashtags=template_data.get('default_hashtags', []),
|
||||
ai_prompt=template_data.get('ai_prompt', ''),
|
||||
ai_tone=template_data.get('ai_tone', ''),
|
||||
ai_target_audience=template_data.get('ai_target_audience', '')
|
||||
)
|
||||
|
||||
session.add(template)
|
||||
session.commit()
|
||||
logger.info(f"Saved content template: {template.name}")
|
||||
return template
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error saving content template: {e}")
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_user_templates(self, user_id: str, category: Optional[ContentCategory] = None) -> List[ContentTemplate]:
|
||||
"""Get content templates for a user."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if not user:
|
||||
return []
|
||||
|
||||
query = session.query(ContentTemplate).filter(
|
||||
and_(
|
||||
ContentTemplate.user_id == user.id,
|
||||
ContentTemplate.is_active == True
|
||||
)
|
||||
)
|
||||
|
||||
if category:
|
||||
query = query.filter_by(category=category)
|
||||
|
||||
return query.order_by(desc(ContentTemplate.average_performance)).all()
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# --- SETTINGS ---
|
||||
|
||||
def save_user_settings(self, user_id: str, settings_data: Dict[str, Any]) -> TwitterSettings:
|
||||
"""Save user Twitter settings."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if not user:
|
||||
raise ValueError(f"User {user_id} not found")
|
||||
|
||||
# Check if settings already exist
|
||||
existing_settings = session.query(TwitterSettings).filter_by(user_id=user.id).first()
|
||||
|
||||
if existing_settings:
|
||||
# Update existing settings
|
||||
for key, value in settings_data.items():
|
||||
if hasattr(existing_settings, key):
|
||||
setattr(existing_settings, key, value)
|
||||
existing_settings.updated_at = datetime.utcnow()
|
||||
session.commit()
|
||||
return existing_settings
|
||||
else:
|
||||
# Create new settings
|
||||
settings = TwitterSettings(
|
||||
user_id=user.id,
|
||||
**settings_data
|
||||
)
|
||||
session.add(settings)
|
||||
session.commit()
|
||||
logger.info(f"Saved settings for user: {user.username}")
|
||||
return settings
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error saving user settings: {e}")
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_user_settings(self, user_id: str) -> Optional[TwitterSettings]:
|
||||
"""Get user Twitter settings."""
|
||||
session = self.get_session()
|
||||
try:
|
||||
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
|
||||
if not user:
|
||||
return None
|
||||
|
||||
return session.query(TwitterSettings).filter_by(user_id=user.id).first()
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# --- UTILITY METHODS ---
|
||||
|
||||
def cleanup_old_data(self, days_old: int = 30) -> Dict[str, int]:
|
||||
"""
|
||||
Clean up old data to maintain database performance.
|
||||
|
||||
Args:
|
||||
days_old: Number of days old data to keep
|
||||
|
||||
Returns:
|
||||
Dictionary with cleanup statistics
|
||||
"""
|
||||
try:
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days_old)
|
||||
|
||||
with self.get_session() as session:
|
||||
# Clean up old analytics data
|
||||
old_analytics = session.query(TwitterAnalytics).filter(
|
||||
TwitterAnalytics.created_at < cutoff_date
|
||||
).count()
|
||||
|
||||
session.query(TwitterAnalytics).filter(
|
||||
TwitterAnalytics.created_at < cutoff_date
|
||||
).delete()
|
||||
|
||||
# Clean up old tweet analytics
|
||||
old_tweet_analytics = session.query(TweetAnalytics).filter(
|
||||
TweetAnalytics.created_at < cutoff_date
|
||||
).count()
|
||||
|
||||
session.query(TweetAnalytics).filter(
|
||||
TweetAnalytics.created_at < cutoff_date
|
||||
).delete()
|
||||
|
||||
session.commit()
|
||||
|
||||
stats = {
|
||||
'old_analytics_removed': old_analytics,
|
||||
'old_tweet_analytics_removed': old_tweet_analytics,
|
||||
'cutoff_date': cutoff_date.isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"Cleaned up old data: {stats}")
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up old data: {e}")
|
||||
return {'error': str(e)}
|
||||
|
||||
def get_database_stats(self) -> Dict[str, int]:
|
||||
"""
|
||||
Get database statistics.
|
||||
|
||||
Returns:
|
||||
Dictionary with database statistics
|
||||
"""
|
||||
try:
|
||||
with self.get_session() as session:
|
||||
stats = {
|
||||
'total_users': session.query(TwitterUser).count(),
|
||||
'total_tweets': session.query(Tweet).count(),
|
||||
'posted_tweets': session.query(Tweet).filter(
|
||||
Tweet.status == TweetStatus.POSTED
|
||||
).count(),
|
||||
'scheduled_tweets': session.query(ScheduledTweet).filter(
|
||||
ScheduledTweet.status == TweetStatus.SCHEDULED
|
||||
).count(),
|
||||
'total_analytics_records': session.query(TwitterAnalytics).count(),
|
||||
'total_templates': session.query(ContentTemplate).count()
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting database stats: {e}")
|
||||
return {'error': str(e)}
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close database connections and clean up resources.
|
||||
"""
|
||||
try:
|
||||
if hasattr(self, 'engine') and self.engine:
|
||||
self.engine.dispose()
|
||||
logger.info("Database connections closed successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing database connections: {e}")
|
||||
|
||||
# Create a global instance for easy access
|
||||
twitter_db = TwitterDatabaseService()
|
||||
|
||||
# Export the service and key functions
|
||||
__all__ = [
|
||||
'TwitterDatabaseService',
|
||||
'twitter_db'
|
||||
]
|
||||
@@ -1,99 +0,0 @@
|
||||
"""AI research module for topic analysis and research."""
|
||||
|
||||
import asyncio
|
||||
from typing import Dict, Any
|
||||
from loguru import logger
|
||||
import sys
|
||||
from ..web_crawlers.async_web_crawler import AsyncWebCrawlerService
|
||||
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
# Configure logger
|
||||
logger.remove()
|
||||
logger.add(
|
||||
"logs/ai_research.log",
|
||||
rotation="500 MB",
|
||||
retention="10 days",
|
||||
level="DEBUG",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
|
||||
)
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level="INFO",
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||
)
|
||||
|
||||
def research_topic(topic: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Research a topic using web crawling and AI analysis.
|
||||
|
||||
Args:
|
||||
topic (str): The topic to research
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Research results including overview, findings, and recommendations
|
||||
"""
|
||||
try:
|
||||
logger.info(f"[research_topic] Starting research for topic: {topic}")
|
||||
|
||||
# Initialize web crawler
|
||||
async def analyze_topic():
|
||||
async with AsyncWebCrawlerService() as crawler:
|
||||
# Perform web research
|
||||
search_results = await crawler.crawl_website(topic)
|
||||
|
||||
if not search_results.get('success'):
|
||||
return {
|
||||
'success': False,
|
||||
'error': search_results.get('error', 'Research failed')
|
||||
}
|
||||
|
||||
# Analyze content with LLM
|
||||
analysis = await crawler.analyze_content_with_llm(
|
||||
search_results['content'],
|
||||
api_key=None, # Should be passed from config
|
||||
gpt_provider="google" # Should be configurable
|
||||
)
|
||||
|
||||
# Structure the response
|
||||
return {
|
||||
'success': True,
|
||||
'data': {
|
||||
'research': {
|
||||
'overview': {
|
||||
'topic': topic,
|
||||
'scope': analysis.get('topics', []),
|
||||
'methodology': 'Web crawling and AI analysis'
|
||||
},
|
||||
'data_quality': {
|
||||
'is_reliable': bool(analysis.get('seo_score', 0) > 0.7)
|
||||
},
|
||||
'analysis_quality': {
|
||||
'is_thorough': bool(len(analysis.get('key_insights', [])) > 5)
|
||||
},
|
||||
'recommendations': analysis.get('recommendations', []),
|
||||
'next_steps': analysis.get('priority_areas', [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Run the async analysis
|
||||
results = asyncio.run(analyze_topic())
|
||||
|
||||
if not results.get('success'):
|
||||
error_msg = results.get('error', 'Research failed')
|
||||
logger.error(f"[research_topic] Research failed: {error_msg}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': error_msg
|
||||
}
|
||||
|
||||
logger.info("[research_topic] Research completed successfully")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Research failed: {str(e)}"
|
||||
logger.error(f"[research_topic] {error_msg}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
"""API Key Manager package for ALwrity."""
|
||||
|
||||
from .manager import APIKeyManager
|
||||
from .api_key_manager import render, check_onboarding_completion, get_onboarding_status, reset_onboarding
|
||||
from .onboarding_progress import (
|
||||
OnboardingProgress,
|
||||
get_onboarding_progress,
|
||||
render_progress_indicator,
|
||||
render_resume_message,
|
||||
StepStatus,
|
||||
StepData
|
||||
)
|
||||
from .validation import check_all_api_keys
|
||||
from .components.base import (
|
||||
render_step_indicator,
|
||||
render_navigation_buttons,
|
||||
render_step_validation,
|
||||
render_resume_options
|
||||
)
|
||||
|
||||
# Export all public components
|
||||
__all__ = [
|
||||
# Main classes
|
||||
'APIKeyManager',
|
||||
'OnboardingProgress',
|
||||
'StepStatus',
|
||||
'StepData',
|
||||
|
||||
# Main functions
|
||||
'render',
|
||||
'check_onboarding_completion',
|
||||
'get_onboarding_status',
|
||||
'reset_onboarding',
|
||||
'get_onboarding_progress',
|
||||
|
||||
# UI components
|
||||
'render_progress_indicator',
|
||||
'render_resume_message',
|
||||
'render_step_indicator',
|
||||
'render_navigation_buttons',
|
||||
'render_step_validation',
|
||||
'render_resume_options',
|
||||
|
||||
# Validation
|
||||
'check_all_api_keys'
|
||||
]
|
||||
|
||||
# Version information
|
||||
__version__ = "2.0.0"
|
||||
__author__ = "ALwrity Team"
|
||||
__description__ = "Comprehensive API key management and onboarding system for ALwrity"
|
||||
|
||||
# Note: FastAPI endpoints have been moved to the backend/ directory
|
||||
# for better separation of concerns and enterprise architecture.
|
||||
@@ -1,42 +0,0 @@
|
||||
"""AI research functionality for API key manager."""
|
||||
|
||||
from loguru import logger
|
||||
import asyncio
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
async def research_topic(topic: str, api_keys: Dict[str, str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Research a topic using available AI services.
|
||||
|
||||
Args:
|
||||
topic (str): The topic to research
|
||||
api_keys (Dict[str, str]): Dictionary of API keys for different services
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Research results and metadata
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starting research on topic: {topic}")
|
||||
|
||||
# TODO: Implement actual research functionality using available API keys
|
||||
# This is a placeholder implementation
|
||||
results = {
|
||||
"topic": topic,
|
||||
"status": "success",
|
||||
"data": {
|
||||
"summary": f"Research summary for {topic}",
|
||||
"key_points": ["Point 1", "Point 2", "Point 3"],
|
||||
"sources": ["Source 1", "Source 2"]
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Research completed successfully")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during research: {str(e)}")
|
||||
return {
|
||||
"topic": topic,
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
# ALwrity Setup Components Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The ALwrity Setup Components are the building blocks that guide you through setting up your content creation environment. Each component is designed to help you configure specific aspects of ALwrity for optimal content creation.
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Website Setup (`website_setup.py`)
|
||||
**Purpose**: Configure your website's basic information and analyze its current state
|
||||
|
||||
**Features**:
|
||||
- **URL Configuration**: Set up your website's URL
|
||||
- **Analysis Options**:
|
||||
- Basic Analysis: Quick overview of your website
|
||||
- Full Analysis with SEO: Comprehensive website and SEO analysis
|
||||
- **Analysis Results**:
|
||||
- Basic Metrics: Status, content type, title, meta description
|
||||
- Content Analysis: Word count, headings, images, links
|
||||
- SEO Analysis: SEO score, meta tags, content quality
|
||||
- Technical SEO: Mobile friendliness, page speed, technical issues
|
||||
- Strategy Recommendations: Actionable improvements
|
||||
|
||||
### 2. AI Research Setup (`ai_research_setup.py`)
|
||||
**Purpose**: Configure AI-powered research tools for content creation
|
||||
|
||||
**Features**:
|
||||
- **Traditional Search**:
|
||||
- SerpAPI integration for real-time search results
|
||||
- Access to structured data and knowledge graphs
|
||||
- News articles and related questions
|
||||
|
||||
- **AI Deep Research**:
|
||||
- Tavily AI for semantic understanding
|
||||
- Metaphor/Exa for neural search capabilities
|
||||
- Advanced research features
|
||||
|
||||
### 3. AI Providers (`ai_providers.py`)
|
||||
**Purpose**: Set up your preferred AI content generation services
|
||||
|
||||
**Supported Providers**:
|
||||
- **OpenAI (GPT models)**
|
||||
- Advanced language models
|
||||
- Creative content generation
|
||||
- Context-aware responses
|
||||
|
||||
- **Google (Gemini Pro)**
|
||||
- Balanced content creation
|
||||
- Factual accuracy
|
||||
- Multilingual support
|
||||
|
||||
- **Anthropic (Claude)**
|
||||
- Professional writing
|
||||
- Detailed analysis
|
||||
- Ethical considerations
|
||||
|
||||
- **DeepSeek**
|
||||
- Technical content
|
||||
- Specialized knowledge
|
||||
- Efficient processing
|
||||
|
||||
### 4. Personalization Setup (`personalization_setup.py`)
|
||||
**Purpose**: Customize your content creation experience
|
||||
|
||||
**Features**:
|
||||
- **Writing Style**:
|
||||
- Tone preferences
|
||||
- Voice settings
|
||||
- Content structure
|
||||
|
||||
- **Brand Configuration**:
|
||||
- Brand voice
|
||||
- Style guidelines
|
||||
- Content templates
|
||||
|
||||
### 5. ALwrity Integrations (`alwrity_integrations.py`)
|
||||
**Purpose**: Connect additional tools and services
|
||||
|
||||
**Features**:
|
||||
- **Third-party Services**:
|
||||
- Analytics integration
|
||||
- Social media tools
|
||||
- Content management systems
|
||||
|
||||
- **Workflow Automation**:
|
||||
- Publishing tools
|
||||
- Content scheduling
|
||||
- Distribution channels
|
||||
|
||||
### 6. Final Setup (`final_setup.py`)
|
||||
**Purpose**: Complete and verify your configuration
|
||||
|
||||
**Features**:
|
||||
- **Configuration Review**:
|
||||
- Settings verification
|
||||
- Connection testing
|
||||
- Setup completion
|
||||
|
||||
- **Validation**:
|
||||
- API key verification
|
||||
- Service connectivity
|
||||
- System readiness
|
||||
|
||||
## Base Components
|
||||
|
||||
### 1. Navigation (`base.py`)
|
||||
**Purpose**: Provide consistent navigation throughout the setup process
|
||||
|
||||
**Features**:
|
||||
- Step indicators
|
||||
- Navigation buttons
|
||||
- Progress tracking
|
||||
- Back/forward controls
|
||||
|
||||
## How to Use the Components
|
||||
|
||||
### 1. Starting the Setup
|
||||
1. Launch ALwrity
|
||||
2. Navigate to the Setup section
|
||||
3. Follow the guided wizard process
|
||||
|
||||
### 2. Component Navigation
|
||||
- Use the step indicator to track progress
|
||||
- Navigate between components using buttons
|
||||
- Save progress automatically
|
||||
- Return to previous steps if needed
|
||||
|
||||
### 3. Configuration Process
|
||||
1. **Enter Information**: Fill in required details
|
||||
2. **Verify Settings**: Review your inputs
|
||||
3. **Test Connections**: Ensure everything works
|
||||
4. **Complete Setup**: Finalize your configuration
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Before Setup
|
||||
- Gather all necessary API keys
|
||||
- Review provider documentation
|
||||
- Plan your configuration
|
||||
- Backup existing settings
|
||||
|
||||
### 2. During Setup
|
||||
- Follow the wizard steps
|
||||
- Verify each configuration
|
||||
- Test connections
|
||||
- Save progress regularly
|
||||
|
||||
### 3. After Setup
|
||||
- Review all settings
|
||||
- Test functionality
|
||||
- Document configurations
|
||||
- Monitor usage
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 1. Common Issues
|
||||
- Invalid API keys
|
||||
- Connection problems
|
||||
- Configuration errors
|
||||
- Setup interruptions
|
||||
|
||||
### 2. Solutions
|
||||
- Key verification
|
||||
- Connection testing
|
||||
- Error logging
|
||||
- Support resources
|
||||
|
||||
## Need Help?
|
||||
|
||||
If you encounter any issues during setup:
|
||||
1. Check the error messages
|
||||
2. Review the documentation
|
||||
3. Verify your API keys
|
||||
4. Contact ALwrity support
|
||||
|
||||
---
|
||||
|
||||
*Note: Each component is designed to help you set up a specific aspect of ALwrity. Follow the setup wizard in order to ensure all components are properly configured for optimal content creation.*
|
||||
@@ -1,22 +0,0 @@
|
||||
"""API key manager components package."""
|
||||
|
||||
from .ai_research_setup import render_ai_research_setup
|
||||
from .ai_research import render_ai_research
|
||||
from .ai_providers import render_ai_providers
|
||||
from .final_setup import render_final_setup
|
||||
from .personalization_setup import render_personalization_setup
|
||||
from .alwrity_integrations import render_alwrity_integrations
|
||||
from .base import render_navigation_buttons, render_step_indicator
|
||||
from .website_setup import render_website_setup
|
||||
|
||||
__all__ = [
|
||||
'render_ai_research_setup',
|
||||
'render_ai_research',
|
||||
'render_ai_providers',
|
||||
'render_final_setup',
|
||||
'render_personalization_setup',
|
||||
'render_alwrity_integrations',
|
||||
'render_navigation_buttons',
|
||||
'render_step_indicator',
|
||||
'render_website_setup'
|
||||
]
|
||||
@@ -1,137 +0,0 @@
|
||||
"""AI Research setup component."""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, Any
|
||||
from loguru import logger
|
||||
from ..manager import APIKeyManager
|
||||
from .base import render_navigation_buttons, render_step_indicator
|
||||
|
||||
def render_ai_research(api_key_manager: APIKeyManager) -> Dict[str, Any]:
|
||||
"""Render the AI Research setup step."""
|
||||
try:
|
||||
st.markdown("""
|
||||
<div class='setup-header'>
|
||||
<h2>🔍 AI Research Configuration</h2>
|
||||
<p>Configure your research preferences and provide user information</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Create tabs for different sections
|
||||
tabs = st.tabs(["User Information", "Research Preferences"])
|
||||
|
||||
changes_made = False
|
||||
has_valid_info = False
|
||||
validation_message = ""
|
||||
|
||||
with tabs[0]:
|
||||
st.markdown("### User Information")
|
||||
st.markdown("Please provide your details for personalized research experience")
|
||||
|
||||
# User Information Card
|
||||
with st.container():
|
||||
st.markdown("""
|
||||
<div class="user-info-card">
|
||||
<div class="user-info-header">
|
||||
<div class="user-info-icon">👤</div>
|
||||
<div class="user-info-title">Personal Details</div>
|
||||
</div>
|
||||
<div class="user-info-content">
|
||||
<p>Your information helps us customize the research experience.</p>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# User Input Fields with Streamlit Components
|
||||
full_name = st.text_input("Full Name", key="full_name",
|
||||
help="Enter your full name as you'd like it to appear")
|
||||
|
||||
email = st.text_input("Email Address", key="email",
|
||||
help="Enter your business email address")
|
||||
|
||||
company = st.text_input("Company/Organization", key="company",
|
||||
help="Enter your company or organization name")
|
||||
|
||||
role = st.selectbox("Role",
|
||||
["Content Creator", "Marketing Manager", "Business Owner", "Other"],
|
||||
help="Select your primary role")
|
||||
|
||||
with tabs[1]:
|
||||
st.markdown("### Research Preferences")
|
||||
st.markdown("Configure how AI assists with your research")
|
||||
|
||||
# Research Preferences Card
|
||||
with st.container():
|
||||
st.markdown("""
|
||||
<div class="research-prefs-card">
|
||||
<div class="research-prefs-header">
|
||||
<div class="research-prefs-icon">🎯</div>
|
||||
<div class="research-prefs-title">Research Settings</div>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Research Preferences Settings
|
||||
research_depth = st.select_slider(
|
||||
"Research Depth",
|
||||
options=["Basic", "Standard", "Deep", "Comprehensive"],
|
||||
value="Standard",
|
||||
help="Choose how detailed you want the AI research to be"
|
||||
)
|
||||
|
||||
st.markdown("#### Content Types")
|
||||
content_types = st.multiselect(
|
||||
"Select content types to focus on",
|
||||
["Blog Posts", "Social Media", "Technical Articles", "News", "Academic Papers"],
|
||||
default=["Blog Posts", "Social Media"],
|
||||
help="Choose what types of content you want to research"
|
||||
)
|
||||
|
||||
auto_research = st.toggle(
|
||||
"Enable Automated Research",
|
||||
help="Automatically start research when content topics are added"
|
||||
)
|
||||
|
||||
# Validate inputs
|
||||
if all([full_name, email, company]):
|
||||
changes_made = True
|
||||
has_valid_info = True
|
||||
validation_message = "✅ User information completed successfully"
|
||||
else:
|
||||
validation_message = "⚠️ Please fill in all required fields to continue"
|
||||
|
||||
# Display validation message
|
||||
if validation_message:
|
||||
if "✅" in validation_message:
|
||||
st.success(validation_message)
|
||||
else:
|
||||
st.warning(validation_message)
|
||||
|
||||
# Navigation buttons
|
||||
if render_navigation_buttons(3, 6, changes_made):
|
||||
if has_valid_info:
|
||||
# Store user information in session state
|
||||
st.session_state['user_info'] = {
|
||||
'full_name': full_name,
|
||||
'email': email,
|
||||
'company': company,
|
||||
'role': role,
|
||||
'research_preferences': {
|
||||
'depth': research_depth,
|
||||
'content_types': content_types,
|
||||
'auto_research': auto_research
|
||||
}
|
||||
}
|
||||
|
||||
# Update progress and move to next step
|
||||
st.session_state['current_step'] = 4
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Please complete all required fields to continue")
|
||||
|
||||
return {"current_step": 3, "changes_made": changes_made}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error in AI research setup: {str(e)}"
|
||||
logger.error(f"[render_ai_research] {error_msg}")
|
||||
st.error(error_msg)
|
||||
return {"current_step": 3, "error": error_msg}
|
||||
@@ -1,188 +0,0 @@
|
||||
"""Personalization setup component."""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, Any
|
||||
from loguru import logger
|
||||
from ..manager import APIKeyManager
|
||||
from .base import render_navigation_buttons, render_step_indicator
|
||||
|
||||
def render_personalization(api_key_manager: APIKeyManager) -> Dict[str, Any]:
|
||||
"""Render the personalization setup step."""
|
||||
try:
|
||||
st.markdown("""
|
||||
<div class='setup-header'>
|
||||
<h2>🎨 Personalization Settings</h2>
|
||||
<p>Customize your content generation experience</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Create tabs for different sections
|
||||
tabs = st.tabs(["Content Style", "Brand Voice", "Advanced Settings"])
|
||||
|
||||
changes_made = False
|
||||
has_valid_settings = False
|
||||
validation_message = ""
|
||||
|
||||
with tabs[0]:
|
||||
st.markdown("### Content Style")
|
||||
st.markdown("Define your preferred content style and tone")
|
||||
|
||||
# Content Style Card
|
||||
with st.container():
|
||||
st.markdown("""
|
||||
<div class="style-card">
|
||||
<div class="style-header">
|
||||
<div class="style-icon">✨</div>
|
||||
<div class="style-title">Writing Style</div>
|
||||
</div>
|
||||
<div class="style-content">
|
||||
<p>Choose how you want your content to be written.</p>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Style Settings
|
||||
writing_style = st.selectbox(
|
||||
"Writing Style",
|
||||
["Professional", "Casual", "Technical", "Conversational", "Academic"],
|
||||
help="Select your preferred writing style"
|
||||
)
|
||||
|
||||
tone = st.select_slider(
|
||||
"Content Tone",
|
||||
options=["Formal", "Semi-Formal", "Neutral", "Friendly", "Humorous"],
|
||||
value="Neutral",
|
||||
help="Choose the tone for your content"
|
||||
)
|
||||
|
||||
content_length = st.select_slider(
|
||||
"Content Length",
|
||||
options=["Concise", "Standard", "Detailed", "Comprehensive"],
|
||||
value="Standard",
|
||||
help="Select your preferred content length"
|
||||
)
|
||||
|
||||
with tabs[1]:
|
||||
st.markdown("### Brand Voice")
|
||||
st.markdown("Configure your brand's unique voice and personality")
|
||||
|
||||
# Brand Voice Card
|
||||
with st.container():
|
||||
st.markdown("""
|
||||
<div class="brand-card">
|
||||
<div class="brand-header">
|
||||
<div class="brand-icon">🎯</div>
|
||||
<div class="brand-title">Brand Identity</div>
|
||||
</div>
|
||||
<div class="brand-content">
|
||||
<p>Define your brand's personality and voice.</p>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Brand Settings
|
||||
brand_personality = st.multiselect(
|
||||
"Brand Personality Traits",
|
||||
["Professional", "Innovative", "Friendly", "Trustworthy", "Creative", "Expert"],
|
||||
default=["Professional", "Trustworthy"],
|
||||
help="Select traits that best describe your brand"
|
||||
)
|
||||
|
||||
brand_voice = st.text_area(
|
||||
"Brand Voice Description",
|
||||
help="Describe how your brand should sound in content"
|
||||
)
|
||||
|
||||
keywords = st.text_input(
|
||||
"Brand Keywords",
|
||||
help="Enter key terms that should be used in your content"
|
||||
)
|
||||
|
||||
with tabs[2]:
|
||||
st.markdown("### Advanced Settings")
|
||||
st.markdown("Fine-tune your content generation preferences")
|
||||
|
||||
# Advanced Settings Card
|
||||
with st.container():
|
||||
st.markdown("""
|
||||
<div class="advanced-card">
|
||||
<div class="advanced-header">
|
||||
<div class="advanced-icon">⚙️</div>
|
||||
<div class="advanced-title">Advanced Options</div>
|
||||
</div>
|
||||
<div class="advanced-content">
|
||||
<p>Configure advanced content generation settings.</p>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Advanced Settings
|
||||
seo_optimization = st.toggle(
|
||||
"Enable SEO Optimization",
|
||||
help="Automatically optimize content for search engines"
|
||||
)
|
||||
|
||||
readability_level = st.select_slider(
|
||||
"Readability Level",
|
||||
options=["Simple", "Standard", "Advanced", "Expert"],
|
||||
value="Standard",
|
||||
help="Choose the complexity level of your content"
|
||||
)
|
||||
|
||||
content_structure = st.multiselect(
|
||||
"Content Structure",
|
||||
["Introduction", "Key Points", "Examples", "Conclusion", "Call-to-Action"],
|
||||
default=["Introduction", "Key Points", "Conclusion"],
|
||||
help="Select required content sections"
|
||||
)
|
||||
|
||||
# Validate settings
|
||||
if all([writing_style, tone, content_length, brand_personality]):
|
||||
changes_made = True
|
||||
has_valid_settings = True
|
||||
validation_message = "✅ Personalization settings completed successfully"
|
||||
else:
|
||||
validation_message = "⚠️ Please complete all required settings to continue"
|
||||
|
||||
# Display validation message
|
||||
if validation_message:
|
||||
if "✅" in validation_message:
|
||||
st.success(validation_message)
|
||||
else:
|
||||
st.warning(validation_message)
|
||||
|
||||
# Navigation buttons
|
||||
if render_navigation_buttons(4, 6, changes_made):
|
||||
if has_valid_settings:
|
||||
# Store personalization settings in session state
|
||||
st.session_state['personalization'] = {
|
||||
'content_style': {
|
||||
'writing_style': writing_style,
|
||||
'tone': tone,
|
||||
'content_length': content_length
|
||||
},
|
||||
'brand_voice': {
|
||||
'personality': brand_personality,
|
||||
'voice_description': brand_voice,
|
||||
'keywords': keywords
|
||||
},
|
||||
'advanced_settings': {
|
||||
'seo_optimization': seo_optimization,
|
||||
'readability_level': readability_level,
|
||||
'content_structure': content_structure
|
||||
}
|
||||
}
|
||||
|
||||
# Update progress and move to next step
|
||||
st.session_state['current_step'] = 5
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Please complete all required settings to continue")
|
||||
|
||||
return {"current_step": 4, "changes_made": changes_made}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error in personalization setup: {str(e)}"
|
||||
logger.error(f"[render_personalization] {error_msg}")
|
||||
st.error(error_msg)
|
||||
return {"current_step": 4, "error": error_msg}
|
||||
@@ -1,79 +0,0 @@
|
||||
import streamlit as st
|
||||
|
||||
from lib.alwrity_ui.similar_analysis import competitor_analysis
|
||||
from lib.alwrity_ui.keyword_web_researcher import do_web_research
|
||||
|
||||
|
||||
def content_planning_tools():
|
||||
# A custom CSS for compact layout
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* Reduce top padding of main container */
|
||||
.main .block-container {
|
||||
padding-top: 0rem !important;
|
||||
padding-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
/* Reduce spacing between elements */
|
||||
.stTabs {
|
||||
margin-top: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Make markdown text more compact */
|
||||
.element-container {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Adjust subheader margins */
|
||||
.stMarkdown h3 {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Make description more compact using a smaller font
|
||||
st.markdown("""
|
||||
<div style='font-size: 0.9em; margin-bottom: 0.5rem;'>
|
||||
<strong>Alwrity content Ideation & Planning</strong>: Provide few keywords to do comprehensive web research.
|
||||
Provide few keywords to get Google, Neural, pytrends analysis. Know keywords, blog titles to target.
|
||||
Generate months long content calendar around given keywords.
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Create tabs with reduced spacing
|
||||
tab_keywords, tab_competitor, tab_calendar = st.tabs([
|
||||
"🔍 Keywords Researcher",
|
||||
"📊 Competitor Analysis",
|
||||
"📅 Content Calendar Ideator"
|
||||
])
|
||||
|
||||
# Keywords Researcher tab
|
||||
with tab_keywords:
|
||||
do_web_research()
|
||||
|
||||
# Competitor Analysis tab
|
||||
with tab_competitor:
|
||||
competitor_analysis()
|
||||
|
||||
# Content Calendar Ideator tab
|
||||
with tab_calendar:
|
||||
st.info("🚧 **Content Calendar & Planning Dashboard**")
|
||||
st.markdown("""
|
||||
<div style='background-color: #f0f2f6; padding: 15px; border-radius: 5px; margin-bottom: 20px;'>
|
||||
<h3 style='margin-top: 0;'>📅 Content Calendar & Planning Dashboard</h3>
|
||||
<p>The Content Calendar Dashboard provides:</p>
|
||||
<ul>
|
||||
<li>AI-powered content planning and generation</li>
|
||||
<li>Multi-platform content scheduling</li>
|
||||
<li>Content optimization tools</li>
|
||||
<li>A/B testing capabilities</li>
|
||||
<li>Performance analytics</li>
|
||||
</ul>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Initialize and render the dashboard directly
|
||||
from lib.ai_seo_tools.content_calendar.ui.dashboard import ContentCalendarDashboard
|
||||
dashboard = ContentCalendarDashboard()
|
||||
dashboard.render()
|
||||
@@ -1,113 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
import subprocess
|
||||
|
||||
from time import sleep
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from PIL import Image
|
||||
|
||||
from selenium import webdriver
|
||||
from PIL import Image
|
||||
import shutil
|
||||
from screenshotone import Client, TakeOptions
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(Path('../.env'))
|
||||
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
|
||||
|
||||
def screenshot_api(url, generated_image_filepath):
|
||||
""" Use screenshotone API to take company webpage screenshots """
|
||||
try:
|
||||
# create API client
|
||||
client = Client(os.getenv('SCREENSHOTONE_ACCESS_KEY'), os.getenv('SCREENSHOTONE_SECRET_KEY'))
|
||||
|
||||
# set up options
|
||||
options = (TakeOptions.url(url)
|
||||
.format("png")
|
||||
.viewport_width(1024)
|
||||
.viewport_height(768)
|
||||
.block_cookie_banners(True)
|
||||
.block_chats(True))
|
||||
|
||||
# generate the screenshot URL and share it with a user
|
||||
#url = client.generate_take_url(options)
|
||||
# or render a screenshot and download the image as stream
|
||||
image = client.take(options)
|
||||
|
||||
# store the screenshot the example.png file
|
||||
with open(generated_image_filepath, 'wb', encoding="utf-8") as result_file:
|
||||
shutil.copyfileobj(image, result_file)
|
||||
|
||||
# Display the screenshot using Image.show
|
||||
image = Image.open(generated_image_filepath)
|
||||
image.show()
|
||||
# Wait for 2 seconds (adjust the delay as needed)
|
||||
sleep(2)
|
||||
# Close the image window
|
||||
image.close()
|
||||
|
||||
except Exception as err:
|
||||
print(f"Failed in screenshotone api: {err}")
|
||||
generated_image_filepath = take_screenshot(url, generated_image_filepath)
|
||||
|
||||
return generated_image_filepath
|
||||
|
||||
|
||||
def take_screenshot(url, generated_image_filepath):
|
||||
# Create a webdriver instance in headless mode
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument("--headless")
|
||||
driver = webdriver.Chrome(options=options)
|
||||
logger.debug(f"Taking screenshot of url: {url}")
|
||||
|
||||
try:
|
||||
# Navigate to the given url
|
||||
driver.get(url)
|
||||
|
||||
# Optionally, increase the delay to ensure all content is loaded
|
||||
sleep(2)
|
||||
|
||||
# Explicitly wait for the page to load (adjust timeout as needed)
|
||||
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
||||
|
||||
# Set a larger window size
|
||||
driver.set_window_size(1200, 800)
|
||||
|
||||
# Take a screenshot of the webpage
|
||||
screenshot = driver.get_screenshot_as_png()
|
||||
|
||||
# Save the screenshot to a file
|
||||
with open(generated_image_filepath, "wb", encoding="utf-8") as f:
|
||||
f.write(screenshot)
|
||||
|
||||
# Display the screenshot using Image.show
|
||||
image = Image.open(generated_image_filepath)
|
||||
image.show()
|
||||
# Wait for 2 seconds (adjust the delay as needed)
|
||||
sleep(2)
|
||||
|
||||
# Close the image window using subprocess (platform-dependent)
|
||||
subprocess.run(["pkill", "-f", "display"]) # Adjust based on your platform and viewer
|
||||
|
||||
# If using macOS, you can use the following:
|
||||
# subprocess.run(["osascript", "-e", 'tell application "Preview" to close every window'])
|
||||
# If using Windows, you can use the following:
|
||||
# subprocess.run(["taskkill", "/F", "/IM", "Microsoft.Photos.exe"])
|
||||
|
||||
logger.debug(f"Screenshot successfully stored at: {generated_image_filepath}")
|
||||
return generated_image_filepath
|
||||
finally:
|
||||
# Close the webdriver instance
|
||||
driver.quit()
|
||||
@@ -1,181 +0,0 @@
|
||||
# Website Analyzer Module
|
||||
|
||||
A comprehensive website analysis toolkit that provides detailed insights into website performance, SEO metrics, and content quality. This module combines traditional web analysis techniques with AI-powered content evaluation to deliver actionable recommendations.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Comprehensive Website Analysis
|
||||
- Basic website information extraction
|
||||
- SSL/TLS certificate validation
|
||||
- DNS record analysis
|
||||
- WHOIS information retrieval
|
||||
- Content analysis and structure evaluation
|
||||
- Performance metrics assessment
|
||||
|
||||
### 2. Advanced SEO Analysis
|
||||
- Meta tag optimization analysis
|
||||
- Content quality evaluation
|
||||
- Keyword density analysis
|
||||
- Readability scoring
|
||||
- Heading structure analysis
|
||||
- AI-powered content recommendations
|
||||
|
||||
### 3. Technical Infrastructure
|
||||
- Asynchronous web crawling
|
||||
- Multi-threaded analysis
|
||||
- Robust error handling
|
||||
- Comprehensive logging
|
||||
- Type-safe data models
|
||||
|
||||
## Module Structure
|
||||
|
||||
### 1. `analyzer.py`
|
||||
The main analysis engine that provides comprehensive website analysis.
|
||||
|
||||
#### Key Components:
|
||||
- `WebsiteAnalyzer` class
|
||||
- URL validation
|
||||
- Basic website information extraction
|
||||
- SSL/TLS certificate checking
|
||||
- DNS record analysis
|
||||
- WHOIS information retrieval
|
||||
- Content analysis
|
||||
- Performance metrics assessment
|
||||
|
||||
#### Features:
|
||||
- Concurrent analysis using ThreadPoolExecutor
|
||||
- Robust error handling and logging
|
||||
- User-agent simulation for reliable scraping
|
||||
- Timeout handling for requests
|
||||
- Comprehensive result formatting
|
||||
|
||||
### 2. `seo_analyzer.py`
|
||||
Specialized SEO analysis module with AI integration.
|
||||
|
||||
#### Key Components:
|
||||
- `extract_content()`: Fetches and parses webpage content
|
||||
- `analyze_meta_tags()`: Evaluates meta tags and SEO elements
|
||||
- `analyze_content_with_ai()`: AI-powered content analysis
|
||||
- `analyze_seo()`: Main SEO analysis function
|
||||
|
||||
#### Features:
|
||||
- Meta tag optimization analysis
|
||||
- Content quality scoring
|
||||
- Keyword density analysis
|
||||
- Readability evaluation
|
||||
- AI-powered recommendations
|
||||
- Weighted scoring system
|
||||
|
||||
### 3. `models.py`
|
||||
Data models for structured analysis results.
|
||||
|
||||
#### Key Components:
|
||||
- `SEORecommendation`: Individual SEO recommendations
|
||||
- `MetaTagAnalysis`: Meta tag analysis results
|
||||
- `ContentAnalysis`: Content analysis metrics
|
||||
- `SEOAnalysisResult`: Complete analysis results
|
||||
|
||||
#### Features:
|
||||
- Type-safe data structures
|
||||
- Clear data organization
|
||||
- Easy serialization/deserialization
|
||||
- Comprehensive documentation
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Website Analysis
|
||||
```python
|
||||
from website_analyzer import analyze_website
|
||||
|
||||
# Analyze a website
|
||||
results = analyze_website("https://example.com")
|
||||
|
||||
# Access analysis results
|
||||
if results["success"]:
|
||||
data = results["data"]
|
||||
print(f"Domain: {data['domain']}")
|
||||
print(f"SSL Info: {data['analysis']['ssl_info']}")
|
||||
print(f"Content Info: {data['analysis']['content_info']}")
|
||||
```
|
||||
|
||||
### SEO Analysis
|
||||
```python
|
||||
from website_analyzer.seo_analyzer import analyze_seo
|
||||
|
||||
# Perform SEO analysis
|
||||
seo_results = analyze_seo("https://example.com", "your-openai-api-key")
|
||||
|
||||
# Access SEO results
|
||||
if seo_results.success:
|
||||
print(f"Overall Score: {seo_results.overall_score}")
|
||||
print(f"Meta Tags: {seo_results.meta_tags}")
|
||||
print(f"Content Analysis: {seo_results.content}")
|
||||
print(f"Recommendations: {seo_results.recommendations}")
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `requests`: HTTP requests
|
||||
- `beautifulsoup4`: HTML parsing
|
||||
- `python-whois`: WHOIS information
|
||||
- `dnspython`: DNS record analysis
|
||||
- `openai`: AI-powered analysis
|
||||
- `loguru`: Logging
|
||||
- `typing`: Type hints
|
||||
- `dataclasses`: Data models
|
||||
|
||||
## Error Handling
|
||||
|
||||
The module implements comprehensive error handling:
|
||||
- URL validation
|
||||
- Request timeouts
|
||||
- Connection errors
|
||||
- Parsing errors
|
||||
- API errors
|
||||
- DNS resolution errors
|
||||
- SSL/TLS errors
|
||||
|
||||
All errors are logged and returned in a structured format for easy handling.
|
||||
|
||||
## Logging
|
||||
|
||||
The module uses `loguru` for logging with the following features:
|
||||
- File rotation (500 MB)
|
||||
- 10-day retention
|
||||
- Debug level logging
|
||||
- Structured log format
|
||||
- Both file and stdout output
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **API Key Management**
|
||||
- Store API keys securely
|
||||
- Use environment variables
|
||||
- Implement rate limiting
|
||||
|
||||
2. **Error Handling**
|
||||
- Always check success status
|
||||
- Handle errors gracefully
|
||||
- Log errors appropriately
|
||||
|
||||
3. **Performance**
|
||||
- Use concurrent analysis
|
||||
- Implement timeouts
|
||||
- Cache results when possible
|
||||
|
||||
4. **Rate Limiting**
|
||||
- Respect website robots.txt
|
||||
- Implement delays between requests
|
||||
- Use appropriate user agents
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Commit your changes
|
||||
4. Push to the branch
|
||||
5. Create a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This module is part of the ALwrity project and is licensed under the MIT License.
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Website analyzer module for AI-powered website analysis."""
|
||||
|
||||
from .analyzer import analyze_website, WebsiteAnalyzer
|
||||
from .models import SEOAnalysisResult
|
||||
|
||||
__all__ = ['analyze_website', 'WebsiteAnalyzer', 'SEOAnalysisResult']
|
||||
@@ -1,697 +0,0 @@
|
||||
"""Website and SEO analysis module."""
|
||||
|
||||
import asyncio
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urljoin, urlparse
|
||||
import streamlit as st
|
||||
import re
|
||||
from loguru import logger
|
||||
from ...web_crawlers.async_web_crawler import AsyncWebCrawlerService
|
||||
from ...gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import json
|
||||
from datetime import datetime
|
||||
import requests
|
||||
import ssl
|
||||
import socket
|
||||
import whois
|
||||
import dns.resolver
|
||||
from requests.exceptions import RequestException
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from .models import (
|
||||
SEOAnalysisResult,
|
||||
MetaTagAnalysis,
|
||||
ContentAnalysis,
|
||||
SEORecommendation
|
||||
)
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler('logs/website_analyzer.log')
|
||||
]
|
||||
)
|
||||
|
||||
# Create a logger for the website analyzer
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Create a separate logger for scraping operations
|
||||
scraping_logger = logging.getLogger('website_analyzer.scraping')
|
||||
scraping_logger.setLevel(logging.WARNING)
|
||||
|
||||
class WebsiteAnalyzer:
|
||||
def __init__(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||
})
|
||||
logger.info("WebsiteAnalyzer initialized")
|
||||
|
||||
def analyze_website(self, url: str) -> Dict:
|
||||
"""
|
||||
Perform comprehensive analysis of a website.
|
||||
|
||||
Args:
|
||||
url (str): The URL to analyze
|
||||
|
||||
Returns:
|
||||
Dict: Analysis results including various metrics and checks
|
||||
"""
|
||||
logger.info(f"Starting analysis for URL: {url}")
|
||||
try:
|
||||
# Validate URL
|
||||
if not self._validate_url(url):
|
||||
error_msg = f"Invalid URL format: {url}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": {"stage": "url_validation"}
|
||||
}
|
||||
|
||||
# Basic URL parsing
|
||||
parsed_url = urlparse(url)
|
||||
domain = parsed_url.netloc
|
||||
|
||||
# Initialize results dictionary
|
||||
results = {
|
||||
"url": url,
|
||||
"domain": domain,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"analysis": {}
|
||||
}
|
||||
|
||||
# Perform various analyses
|
||||
with ThreadPoolExecutor(max_workers=4) as executor:
|
||||
logger.info("Starting parallel analysis tasks")
|
||||
|
||||
# Basic website info
|
||||
logger.info("Starting basic info analysis")
|
||||
basic_info = executor.submit(self._get_basic_info, url).result()
|
||||
if "error" in basic_info:
|
||||
error_msg = f"Basic info analysis failed: {basic_info['error']}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"stage": "basic_info",
|
||||
"details": basic_info.get("error_details", {})
|
||||
}
|
||||
}
|
||||
results["analysis"]["basic_info"] = basic_info
|
||||
|
||||
# SSL/TLS info
|
||||
logger.info("Starting SSL analysis")
|
||||
ssl_info = executor.submit(self._check_ssl, domain).result()
|
||||
results["analysis"]["ssl_info"] = ssl_info
|
||||
|
||||
# DNS info
|
||||
logger.info("Starting DNS analysis")
|
||||
dns_info = executor.submit(self._check_dns, domain).result()
|
||||
results["analysis"]["dns_info"] = dns_info
|
||||
|
||||
# WHOIS info
|
||||
logger.info("Starting WHOIS analysis")
|
||||
whois_info = executor.submit(self._get_whois_info, domain).result()
|
||||
results["analysis"]["whois_info"] = whois_info
|
||||
|
||||
# Content analysis
|
||||
logger.info("Starting content analysis")
|
||||
content_info = executor.submit(self._analyze_content, url).result()
|
||||
if "error" in content_info:
|
||||
error_msg = f"Content analysis failed: {content_info['error']}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"stage": "content_analysis",
|
||||
"details": content_info.get("error_details", {})
|
||||
}
|
||||
}
|
||||
results["analysis"]["content_info"] = content_info
|
||||
|
||||
# Performance metrics
|
||||
logger.info("Starting performance analysis")
|
||||
performance = executor.submit(self._check_performance, url).result()
|
||||
if "error" in performance:
|
||||
error_msg = f"Performance analysis failed: {performance['error']}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"stage": "performance_analysis",
|
||||
"details": performance.get("error_details", {})
|
||||
}
|
||||
}
|
||||
results["analysis"]["performance"] = performance
|
||||
|
||||
# SEO analysis
|
||||
logger.info("Starting SEO analysis")
|
||||
seo_analysis = executor.submit(self._analyze_seo, url).result()
|
||||
if "error" in seo_analysis:
|
||||
error_msg = f"SEO analysis failed: {seo_analysis['error']}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"stage": "seo_analysis",
|
||||
"details": seo_analysis.get("error_details", {})
|
||||
}
|
||||
}
|
||||
results["analysis"]["seo_info"] = seo_analysis
|
||||
|
||||
logger.info(f"Analysis completed successfully for {url}")
|
||||
logger.debug(f"Final results: {json.dumps(results, indent=2)}")
|
||||
return {
|
||||
"success": True,
|
||||
"data": results
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error during website analysis: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"type": type(e).__name__,
|
||||
"traceback": str(e.__traceback__)
|
||||
}
|
||||
}
|
||||
|
||||
def _validate_url(self, url: str) -> bool:
|
||||
"""Validate URL format."""
|
||||
try:
|
||||
result = urlparse(url)
|
||||
return all([result.scheme, result.netloc])
|
||||
except Exception as e:
|
||||
logger.error(f"URL validation error: {str(e)}")
|
||||
return False
|
||||
|
||||
def _get_basic_info(self, url: str) -> Dict:
|
||||
"""Get basic website information."""
|
||||
scraping_logger.debug(f"Getting basic info for {url}")
|
||||
try:
|
||||
response = self.session.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
return {
|
||||
"status_code": response.status_code,
|
||||
"content_type": response.headers.get('content-type', ''),
|
||||
"title": soup.title.string if soup.title else '',
|
||||
"meta_description": self._get_meta_description(soup),
|
||||
"headers": dict(response.headers),
|
||||
"robots_txt": self._get_robots_txt(url),
|
||||
"sitemap": self._get_sitemap(url)
|
||||
}
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = f"Request error in basic info: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"type": "RequestException",
|
||||
"status_code": getattr(e.response, 'status_code', None) if hasattr(e, 'response') else None,
|
||||
"url": url
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = f"Error getting basic info: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"type": type(e).__name__,
|
||||
"traceback": str(e.__traceback__)
|
||||
}
|
||||
}
|
||||
|
||||
def _check_ssl(self, domain: str) -> Dict:
|
||||
"""Check SSL/TLS certificate information."""
|
||||
scraping_logger.debug(f"Checking SSL for {domain}")
|
||||
try:
|
||||
context = ssl.create_default_context()
|
||||
with socket.create_connection((domain, 443)) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=domain) as ssock:
|
||||
cert = ssock.getpeercert()
|
||||
return {
|
||||
"has_ssl": True,
|
||||
"issuer": dict(x[0] for x in cert['issuer']),
|
||||
"expiry": datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z').isoformat(),
|
||||
"version": cert['version'],
|
||||
"subject": dict(x[0] for x in cert['subject'])
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"SSL check error: {str(e)}", exc_info=True)
|
||||
return {"has_ssl": False, "error": str(e)}
|
||||
|
||||
def _check_dns(self, domain: str) -> Dict:
|
||||
"""Check DNS records."""
|
||||
scraping_logger.debug(f"Checking DNS for {domain}")
|
||||
try:
|
||||
records = {}
|
||||
for record_type in ['A', 'AAAA', 'MX', 'NS', 'TXT']:
|
||||
try:
|
||||
answers = dns.resolver.resolve(domain, record_type)
|
||||
records[record_type] = [str(rdata) for rdata in answers]
|
||||
except dns.resolver.NoAnswer:
|
||||
records[record_type] = []
|
||||
except Exception as e:
|
||||
scraping_logger.warning(f"Error resolving {record_type} record: {str(e)}")
|
||||
records[record_type] = []
|
||||
return records
|
||||
except Exception as e:
|
||||
logger.error(f"DNS check error: {str(e)}", exc_info=True)
|
||||
return {"error": str(e)}
|
||||
|
||||
def _get_whois_info(self, domain: str) -> Dict:
|
||||
"""Get WHOIS information for a domain."""
|
||||
scraping_logger.debug(f"Getting WHOIS info for {domain}")
|
||||
try:
|
||||
w = whois.whois(domain)
|
||||
|
||||
def format_date(date_value):
|
||||
if isinstance(date_value, list):
|
||||
return date_value[0].isoformat() if date_value else 'Unknown'
|
||||
return date_value.isoformat() if date_value else 'Unknown'
|
||||
|
||||
return {
|
||||
'registrar': w.registrar if hasattr(w, 'registrar') else 'Unknown',
|
||||
'creation_date': format_date(w.creation_date),
|
||||
'expiration_date': format_date(w.expiration_date),
|
||||
'updated_date': format_date(w.updated_date) if hasattr(w, 'updated_date') else 'Unknown',
|
||||
'name_servers': w.name_servers if hasattr(w, 'name_servers') else [],
|
||||
'domain_name': w.domain_name if hasattr(w, 'domain_name') else domain,
|
||||
'text': w.text if hasattr(w, 'text') else ''
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"WHOIS check error: {str(e)}")
|
||||
return {
|
||||
'registrar': 'Unknown',
|
||||
'creation_date': 'Unknown',
|
||||
'expiration_date': 'Unknown',
|
||||
'updated_date': 'Unknown',
|
||||
'name_servers': [],
|
||||
'domain_name': domain,
|
||||
'text': ''
|
||||
}
|
||||
|
||||
def _analyze_content(self, url: str) -> Dict:
|
||||
"""Analyze website content."""
|
||||
scraping_logger.debug(f"Analyzing content for {url}")
|
||||
try:
|
||||
response = self.session.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
# Get all text content
|
||||
text_content = soup.get_text()
|
||||
|
||||
# Count words
|
||||
words = re.findall(r'\w+', text_content.lower())
|
||||
word_count = len(words)
|
||||
|
||||
# Count headings
|
||||
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
||||
heading_counts = {
|
||||
'h1': len(soup.find_all('h1')),
|
||||
'h2': len(soup.find_all('h2')),
|
||||
'h3': len(soup.find_all('h3')),
|
||||
'h4': len(soup.find_all('h4')),
|
||||
'h5': len(soup.find_all('h5')),
|
||||
'h6': len(soup.find_all('h6'))
|
||||
}
|
||||
|
||||
# Count images
|
||||
images = soup.find_all('img')
|
||||
|
||||
# Count links
|
||||
links = soup.find_all('a')
|
||||
|
||||
# Count paragraphs
|
||||
paragraphs = soup.find_all('p')
|
||||
|
||||
return {
|
||||
"word_count": word_count,
|
||||
"heading_count": len(headings),
|
||||
"heading_structure": heading_counts,
|
||||
"image_count": len(images),
|
||||
"link_count": len(links),
|
||||
"paragraph_count": len(paragraphs),
|
||||
"has_meta_description": bool(self._get_meta_description(soup)),
|
||||
"has_robots_txt": bool(self._get_robots_txt(url)),
|
||||
"has_sitemap": bool(self._get_sitemap(url))
|
||||
}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Request error in content analysis: {str(e)}", exc_info=True)
|
||||
return {
|
||||
"word_count": 0,
|
||||
"heading_count": 0,
|
||||
"heading_structure": {'h1': 0, 'h2': 0, 'h3': 0, 'h4': 0, 'h5': 0, 'h6': 0},
|
||||
"image_count": 0,
|
||||
"link_count": 0,
|
||||
"paragraph_count": 0,
|
||||
"has_meta_description": False,
|
||||
"has_robots_txt": False,
|
||||
"has_sitemap": False,
|
||||
"error": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Content analysis error: {str(e)}", exc_info=True)
|
||||
return {
|
||||
"word_count": 0,
|
||||
"heading_count": 0,
|
||||
"heading_structure": {'h1': 0, 'h2': 0, 'h3': 0, 'h4': 0, 'h5': 0, 'h6': 0},
|
||||
"image_count": 0,
|
||||
"link_count": 0,
|
||||
"paragraph_count": 0,
|
||||
"has_meta_description": False,
|
||||
"has_robots_txt": False,
|
||||
"has_sitemap": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _check_performance(self, url: str) -> Dict:
|
||||
"""Check website performance metrics."""
|
||||
scraping_logger.debug(f"Checking performance for {url}")
|
||||
try:
|
||||
start_time = datetime.now()
|
||||
response = self.session.get(url, timeout=10)
|
||||
end_time = datetime.now()
|
||||
|
||||
load_time = (end_time - start_time).total_seconds()
|
||||
|
||||
return {
|
||||
"load_time": load_time,
|
||||
"status_code": response.status_code,
|
||||
"content_length": len(response.content),
|
||||
"headers": dict(response.headers),
|
||||
"response_time": response.elapsed.total_seconds()
|
||||
}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Request error in performance check: {str(e)}", exc_info=True)
|
||||
return {
|
||||
"load_time": 0,
|
||||
"status_code": 0,
|
||||
"content_length": 0,
|
||||
"headers": {},
|
||||
"response_time": 0,
|
||||
"error": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Performance check error: {str(e)}", exc_info=True)
|
||||
return {
|
||||
"load_time": 0,
|
||||
"status_code": 0,
|
||||
"content_length": 0,
|
||||
"headers": {},
|
||||
"response_time": 0,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _get_meta_description(self, soup: BeautifulSoup) -> Optional[str]:
|
||||
"""Extract meta description from HTML."""
|
||||
meta_desc = soup.find('meta', attrs={'name': 'description'})
|
||||
return meta_desc.get('content') if meta_desc else None
|
||||
|
||||
def _get_robots_txt(self, url: str) -> Optional[str]:
|
||||
"""Get robots.txt content."""
|
||||
try:
|
||||
robots_url = f"{url.rstrip('/')}/robots.txt"
|
||||
response = self.session.get(robots_url, timeout=5)
|
||||
if response.status_code == 200:
|
||||
return response.text
|
||||
except Exception as e:
|
||||
scraping_logger.warning(f"Error fetching robots.txt: {str(e)}")
|
||||
return None
|
||||
|
||||
def _get_sitemap(self, url: str) -> Optional[str]:
|
||||
"""Get sitemap.xml content."""
|
||||
try:
|
||||
sitemap_url = f"{url.rstrip('/')}/sitemap.xml"
|
||||
response = self.session.get(sitemap_url, timeout=5)
|
||||
if response.status_code == 200:
|
||||
return response.text
|
||||
except Exception as e:
|
||||
scraping_logger.warning(f"Error fetching sitemap.xml: {str(e)}")
|
||||
return None
|
||||
|
||||
def _analyze_seo(self, url: str) -> Dict:
|
||||
"""Analyze website SEO."""
|
||||
try:
|
||||
# Extract content
|
||||
content, soup, extract_errors = self._extract_content(url)
|
||||
if not content or not soup:
|
||||
return {
|
||||
"error": "Failed to extract content",
|
||||
"error_details": {"errors": extract_errors}
|
||||
}
|
||||
|
||||
# Analyze meta tags
|
||||
meta_analysis = self._analyze_meta_tags(soup)
|
||||
|
||||
# Analyze content with AI
|
||||
content_analysis, recommendations = self._analyze_content_with_ai(content)
|
||||
|
||||
# Calculate overall score
|
||||
meta_score = sum([
|
||||
1 if meta_analysis.title['status'] == 'good' else 0,
|
||||
1 if meta_analysis.description['status'] == 'good' else 0,
|
||||
1 if meta_analysis.keywords['status'] == 'good' else 0,
|
||||
1 if meta_analysis.has_robots else 0,
|
||||
1 if meta_analysis.has_sitemap else 0
|
||||
]) * 20 # Scale to 100
|
||||
|
||||
overall_score = (
|
||||
meta_score * 0.3 + # 30% weight for meta tags
|
||||
content_analysis.readability_score * 0.3 + # 30% weight for readability
|
||||
content_analysis.content_quality_score * 0.4 # 40% weight for content quality
|
||||
)
|
||||
|
||||
return {
|
||||
"overall_score": overall_score,
|
||||
"meta_tags": meta_analysis.__dict__,
|
||||
"content": content_analysis.__dict__,
|
||||
"recommendations": [rec.__dict__ for rec in recommendations]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error in SEO analysis: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"type": type(e).__name__,
|
||||
"traceback": str(e.__traceback__)
|
||||
}
|
||||
}
|
||||
|
||||
def _extract_content(self, url: str) -> Tuple[Optional[str], Optional[BeautifulSoup], List[str]]:
|
||||
"""Extract content from URL."""
|
||||
errors = []
|
||||
try:
|
||||
response = self.session.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
return response.text, soup, errors
|
||||
except requests.RequestException as e:
|
||||
error_msg = f"Error fetching URL: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
errors.append(error_msg)
|
||||
return None, None, errors
|
||||
|
||||
def _analyze_meta_tags(self, soup: BeautifulSoup) -> MetaTagAnalysis:
|
||||
"""Analyze meta tags using BeautifulSoup."""
|
||||
# Title analysis
|
||||
title = soup.title.string if soup.title else ""
|
||||
title_analysis = {
|
||||
'status': 'good' if title and 30 <= len(title) <= 60 else 'needs_improvement',
|
||||
'value': title,
|
||||
'recommendation': '' if title and 30 <= len(title) <= 60 else 'Title should be between 30-60 characters'
|
||||
}
|
||||
|
||||
# Meta description analysis
|
||||
meta_desc = soup.find('meta', attrs={'name': 'description'})
|
||||
desc = meta_desc.get('content', '') if meta_desc else ""
|
||||
desc_analysis = {
|
||||
'status': 'good' if desc and 120 <= len(desc) <= 160 else 'needs_improvement',
|
||||
'value': desc,
|
||||
'recommendation': '' if desc and 120 <= len(desc) <= 160 else 'Description should be between 120-160 characters'
|
||||
}
|
||||
|
||||
# Keywords analysis
|
||||
meta_keywords = soup.find('meta', attrs={'name': 'keywords'})
|
||||
keywords = meta_keywords.get('content', '') if meta_keywords else ""
|
||||
keywords_analysis = {
|
||||
'status': 'good' if keywords else 'needs_improvement',
|
||||
'value': keywords,
|
||||
'recommendation': '' if keywords else 'Add relevant keywords meta tag'
|
||||
}
|
||||
|
||||
return MetaTagAnalysis(
|
||||
title=title_analysis,
|
||||
description=desc_analysis,
|
||||
keywords=keywords_analysis,
|
||||
has_robots=bool(soup.find('meta', attrs={'name': 'robots'})),
|
||||
has_sitemap=bool(soup.find('link', attrs={'rel': 'sitemap'}))
|
||||
)
|
||||
|
||||
def _analyze_content_with_ai(self, content: str) -> Tuple[ContentAnalysis, List[SEORecommendation]]:
|
||||
"""Analyze content using AI."""
|
||||
try:
|
||||
# Prepare prompt for content analysis
|
||||
prompt = f"""Analyze the following webpage content for SEO and provide a structured analysis:
|
||||
Content: {content[:4000]}... # Truncate to avoid token limits
|
||||
|
||||
Provide analysis in the following format:
|
||||
1. Word count
|
||||
2. Heading structure analysis
|
||||
3. Keyword density for main topics
|
||||
4. Readability score (0-100)
|
||||
5. Content quality score (0-100)
|
||||
6. List of SEO recommendations with priority (high/medium/low), category, issue, recommendation, and impact
|
||||
|
||||
Format the response as JSON."""
|
||||
|
||||
try:
|
||||
# Get AI analysis using llm_text_gen
|
||||
analysis = llm_text_gen(
|
||||
prompt=prompt,
|
||||
system_prompt="You are an SEO expert analyzing website content.",
|
||||
response_format="json_object"
|
||||
)
|
||||
|
||||
if not analysis:
|
||||
logger.error("Empty response from AI analysis")
|
||||
return self._get_fallback_analysis(content)
|
||||
|
||||
# Create ContentAnalysis object
|
||||
content_analysis = ContentAnalysis(
|
||||
word_count=len(content.split()),
|
||||
headings_structure=analysis.get('heading_structure', {}),
|
||||
keyword_density=analysis.get('keyword_density', {}),
|
||||
readability_score=analysis.get('readability_score', 0),
|
||||
content_quality_score=analysis.get('content_quality_score', 0)
|
||||
)
|
||||
|
||||
# Create recommendations
|
||||
recommendations = [
|
||||
SEORecommendation(
|
||||
priority=rec['priority'],
|
||||
category=rec['category'],
|
||||
issue=rec['issue'],
|
||||
recommendation=rec['recommendation'],
|
||||
impact=rec['impact']
|
||||
)
|
||||
for rec in analysis.get('recommendations', [])
|
||||
]
|
||||
|
||||
return content_analysis, recommendations
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in AI analysis: {str(e)}")
|
||||
return self._get_fallback_analysis(content)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in AI analysis setup: {str(e)}")
|
||||
return self._get_fallback_analysis(content)
|
||||
|
||||
def _get_fallback_analysis(self, content: str) -> Tuple[ContentAnalysis, List[SEORecommendation]]:
|
||||
"""Provide fallback analysis when AI analysis is not available."""
|
||||
try:
|
||||
# Basic content analysis
|
||||
words = content.split()
|
||||
word_count = len(words)
|
||||
|
||||
# Simple readability score based on word count
|
||||
readability_score = min(100, max(0, word_count / 10))
|
||||
|
||||
# Basic content quality score
|
||||
content_quality_score = min(100, max(0, word_count / 20))
|
||||
|
||||
# Create basic recommendations
|
||||
recommendations = [
|
||||
SEORecommendation(
|
||||
priority="high",
|
||||
category="content",
|
||||
issue="AI analysis unavailable",
|
||||
recommendation="Consider running the analysis again with a valid API key for more detailed insights",
|
||||
impact="Limited analysis capabilities"
|
||||
)
|
||||
]
|
||||
|
||||
return ContentAnalysis(
|
||||
word_count=word_count,
|
||||
headings_structure={},
|
||||
keyword_density={},
|
||||
readability_score=readability_score,
|
||||
content_quality_score=content_quality_score
|
||||
), recommendations
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in fallback analysis: {str(e)}")
|
||||
return ContentAnalysis(
|
||||
word_count=0,
|
||||
headings_structure={},
|
||||
keyword_density={},
|
||||
readability_score=0,
|
||||
content_quality_score=0
|
||||
), []
|
||||
|
||||
def analyze_website(url: str) -> Dict:
|
||||
"""
|
||||
Analyze a website and return comprehensive results.
|
||||
|
||||
Args:
|
||||
url (str): The URL to analyze
|
||||
|
||||
Returns:
|
||||
Dict: Analysis results including various metrics and checks
|
||||
"""
|
||||
logger.info(f"Starting website analysis for URL: {url}")
|
||||
try:
|
||||
analyzer = WebsiteAnalyzer()
|
||||
|
||||
results = analyzer.analyze_website(url)
|
||||
|
||||
# Add success status to results
|
||||
if "error" in results:
|
||||
error_msg = f"Error in base analysis: {results['error']}"
|
||||
logger.error(error_msg)
|
||||
logger.error(f"Error details: {json.dumps(results.get('error_details', {}), indent=2)}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": results.get("error_details", {})
|
||||
}
|
||||
|
||||
# Add success status and wrap results
|
||||
logger.info("Analysis completed successfully")
|
||||
logger.debug(f"Analysis results: {json.dumps(results, indent=2)}")
|
||||
return {
|
||||
"success": True,
|
||||
"data": results
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = f"Error in analyze_website: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"type": type(e).__name__,
|
||||
"traceback": str(e.__traceback__)
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
from typing import Dict
|
||||
import json
|
||||
|
||||
class ContentGapAnalyzer:
|
||||
def __init__(self, analyzer):
|
||||
self.analyzer = analyzer
|
||||
|
||||
def analyze(self, url: str) -> Dict:
|
||||
"""
|
||||
Analyze content gaps for a given URL.
|
||||
|
||||
Args:
|
||||
url (str): The URL to analyze
|
||||
|
||||
Returns:
|
||||
Dict: Analysis results including content gaps and recommendations
|
||||
"""
|
||||
try:
|
||||
# Get base analysis
|
||||
logger.info(f"Starting content gap analysis for URL: {url}")
|
||||
base_analysis = self.analyzer.analyze_website(url)
|
||||
|
||||
# Check for errors in base analysis
|
||||
if not base_analysis.get("success", False):
|
||||
error_msg = base_analysis.get("error", "Unknown error in website analysis")
|
||||
error_details = base_analysis.get("error_details", {})
|
||||
logger.error(f"Base analysis failed: {error_msg}")
|
||||
logger.error(f"Error details: {json.dumps(error_details, indent=2)}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": error_details,
|
||||
"stage": "base_analysis"
|
||||
}
|
||||
|
||||
# Extract required sections
|
||||
analysis_data = base_analysis.get("data", {}).get("analysis", {})
|
||||
required_sections = ["content_info", "basic_info", "performance"]
|
||||
missing_sections = [section for section in required_sections if section not in analysis_data]
|
||||
|
||||
if missing_sections:
|
||||
error_msg = f"Missing required analysis sections: {', '.join(missing_sections)}"
|
||||
logger.error(error_msg)
|
||||
logger.error(f"Available sections: {list(analysis_data.keys())}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"missing_sections": missing_sections,
|
||||
"available_sections": list(analysis_data.keys())
|
||||
},
|
||||
"stage": "section_validation"
|
||||
}
|
||||
|
||||
# Extract content metrics
|
||||
try:
|
||||
content_info = analysis_data["content_info"]
|
||||
basic_info = analysis_data["basic_info"]
|
||||
performance = analysis_data["performance"]
|
||||
except KeyError as e:
|
||||
error_msg = f"Error extracting analysis section: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"type": "KeyError",
|
||||
"missing_key": str(e),
|
||||
"available_keys": list(analysis_data.keys())
|
||||
},
|
||||
"stage": "data_extraction"
|
||||
}
|
||||
|
||||
# Analyze content gaps
|
||||
try:
|
||||
gaps = self._analyze_content_gaps(content_info, basic_info, performance)
|
||||
except Exception as e:
|
||||
error_msg = f"Error analyzing content gaps: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"type": type(e).__name__,
|
||||
"traceback": str(e.__traceback__)
|
||||
},
|
||||
"stage": "gap_analysis"
|
||||
}
|
||||
|
||||
# Generate recommendations
|
||||
try:
|
||||
recommendations = self._generate_recommendations(gaps)
|
||||
except Exception as e:
|
||||
error_msg = f"Error generating recommendations: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"type": type(e).__name__,
|
||||
"traceback": str(e.__traceback__)
|
||||
},
|
||||
"stage": "recommendation_generation"
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"content_gaps": gaps,
|
||||
"recommendations": recommendations,
|
||||
"metrics": {
|
||||
"word_count": content_info.get("word_count", 0),
|
||||
"heading_count": content_info.get("heading_count", 0),
|
||||
"image_count": content_info.get("image_count", 0),
|
||||
"link_count": content_info.get("link_count", 0),
|
||||
"paragraph_count": content_info.get("paragraph_count", 0),
|
||||
"load_time": performance.get("load_time", 0),
|
||||
"response_time": performance.get("response_time", 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error in content gap analysis: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"error_details": {
|
||||
"type": type(e).__name__,
|
||||
"traceback": str(e.__traceback__)
|
||||
},
|
||||
"stage": "general"
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 270 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 188 KiB |
5
tatus
5
tatus
@@ -1,5 +0,0 @@
|
||||
[33mcc8f9cd2e[m[33m ([m[1;36mHEAD[m[33m -> [m[1;32mmain[m[33m, [m[1;31morigin/cleanup/remove-cache-files[m[33m, [m[1;32mcleanup/remove-cache-files[m[33m)[m Clean up: Remove all cache files and add comprehensive .gitignore
|
||||
[33mc19fc3f22[m[33m ([m[1;31morigin/main[m[33m, [m[1;31morigin/HEAD[m[33m)[m ALwrity Prompts - AI Integration Plan
|
||||
[33m5efee4235[m Added citation and quality metrics to the content editor.
|
||||
[33m10b50f973[m Alwrity Copilot Integration for LinkedIn Writer
|
||||
[33m64944104a[m merge: LinkedIn Writer PR #223 - resolve conflicts and integrate with existing routers
|
||||
Reference in New Issue
Block a user