Alpha Subscription Implementation Plan

This commit is contained in:
ajaysi
2025-09-24 16:08:24 +05:30
parent 580282baa1
commit ac307683e0
57 changed files with 0 additions and 22102 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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']}")

View File

@@ -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

View File

@@ -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}")

View File

@@ -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"))

View File

@@ -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']}")

View File

@@ -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

View File

@@ -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"![{section}]({content.image_path})\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

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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 ""