AI Backlinker, Google Ads Generator, Letter Writer - WIP

This commit is contained in:
ajaysi
2025-05-06 22:27:43 +05:30
parent 26b02b9719
commit 5f7d319859
38 changed files with 14572 additions and 302 deletions

View File

@@ -1,7 +1,7 @@
import streamlit as st
import pandas as pd
from st_aggrid import AgGrid, GridOptionsBuilder, GridUpdateMode
from lib.ai_marketing_tools.ai_backlinking import find_backlink_opportunities, compose_personalized_email
from lib.ai_marketing_tools.ai_backlinker.ai_backlinking import find_backlink_opportunities, compose_personalized_email
# Streamlit UI function

View File

@@ -0,0 +1,370 @@
Google Ads Generator
Google Ads Generator Logo
Overview
The Google Ads Generator is an AI-powered tool designed to create high-converting Google Ads based on industry best practices. This tool helps marketers, business owners, and advertising professionals create optimized ad campaigns that maximize ROI and conversion rates.
By leveraging advanced AI algorithms and proven advertising frameworks, the Google Ads Generator creates compelling ad copy, suggests optimal keywords, generates relevant extensions, and provides performance predictions—all tailored to your specific business needs and target audience.
Table of Contents
Features
Getting Started
User Interface
Ad Creation Process
Ad Types
Quality Analysis
Performance Simulation
Best Practices
Export Options
Advanced Features
Technical Details
FAQ
Troubleshooting
Updates and Roadmap
Features
Core Features
AI-Powered Ad Generation: Create compelling, high-converting Google Ads in seconds
Multiple Ad Types: Support for Responsive Search Ads, Expanded Text Ads, Call-Only Ads, and Dynamic Search Ads
Industry-Specific Templates: Tailored templates for 20+ industries
Ad Extensions Generator: Automatically create Sitelinks, Callouts, and Structured Snippets
Quality Score Analysis: Comprehensive scoring based on Google's quality factors
Performance Prediction: Estimate CTR, conversion rates, and ROI
A/B Testing: Generate multiple variations for testing
Export Options: Export to CSV, Excel, Google Ads Editor CSV, and JSON
Advanced Features
Keyword Research Integration: Find high-performing keywords for your ads
Competitor Analysis: Analyze competitor ads and identify opportunities
Landing Page Suggestions: Recommendations for landing page optimization
Budget Optimization: Suggestions for optimal budget allocation
Ad Schedule Recommendations: Identify the best times to run your ads
Audience Targeting Suggestions: Recommendations for demographic targeting
Local Ad Optimization: Special features for local businesses
E-commerce Ad Features: Product-specific ad generation
Getting Started
Prerequisites
Alwrity AI Writer platform
Basic understanding of Google Ads concepts
Information about your business, products/services, and target audience
Accessing the Tool
Navigate to the Alwrity AI Writer platform
Select "AI Google Ads Generator" from the tools menu
Follow the guided setup process
User Interface
The Google Ads Generator features a user-friendly, tabbed interface designed to guide you through the ad creation process:
Tab 1: Ad Creation
This is where you'll input your business information and ad requirements:
Business Information: Company name, industry, products/services
Campaign Goals: Select from options like brand awareness, lead generation, sales, etc.
Target Audience: Define your ideal customer
Ad Type Selection: Choose from available ad formats
USP and Benefits: Input your unique selling propositions and key benefits
Keywords: Add target keywords or generate suggestions
Landing Page URL: Specify where users will go after clicking your ad
Budget Information: Set daily/monthly budget for performance predictions
Tab 2: Ad Performance
After generating ads, this tab provides detailed analysis:
Quality Score: Overall score (1-10) with detailed breakdown
Strengths & Improvements: What's good and what could be better
Keyword Relevance: Analysis of keyword usage in ad elements
CTR Prediction: Estimated click-through rate based on ad quality
Conversion Potential: Estimated conversion rate
Mobile Friendliness: Assessment of how well the ad performs on mobile
Ad Policy Compliance: Check for potential policy violations
Tab 3: Ad History
Keep track of your generated ads:
Saved Ads: Previously generated and saved ads
Favorites: Ads you've marked as favorites
Version History: Track changes and iterations
Performance Notes: Add notes about real-world performance
Tab 4: Best Practices
Educational resources to improve your ads:
Industry Guidelines: Best practices for your specific industry
Ad Type Tips: Specific guidance for each ad type
Quality Score Optimization: How to improve quality score
Extension Strategies: How to effectively use ad extensions
A/B Testing Guide: How to test and optimize your ads
Ad Creation Process
Step 1: Define Your Campaign
Select your industry from the dropdown menu
Choose your primary campaign goal
Define your target audience
Set your budget parameters
Step 2: Input Business Details
Enter your business name
Provide your website URL
Input your unique selling propositions
List key product/service benefits
Add any promotional offers or discounts
Step 3: Keyword Selection
Enter your primary keywords
Use the integrated keyword research tool to find additional keywords
Select keyword match types (broad, phrase, exact)
Review keyword competition and volume metrics
Step 4: Ad Type Selection
Choose your preferred ad type
Review the requirements and limitations for that ad type
Select any additional features specific to that ad type
Step 5: Generate Ads
Click the "Generate Ads" button
Review the generated ads
Request variations if needed
Save your favorite versions
Step 6: Add Extensions
Select which extension types to include
Review and edit the generated extensions
Add any custom extensions
Step 7: Analyze and Optimize
Review the quality score and analysis
Make suggested improvements
Regenerate ads if necessary
Compare different versions
Step 8: Export
Choose your preferred export format
Select which ads to include
Download the file for import into Google Ads
Ad Types
Responsive Search Ads (RSA)
The most flexible and recommended ad type, featuring:
Up to 15 headlines (3 shown at a time)
Up to 4 descriptions (2 shown at a time)
Dynamic combination of elements based on performance
Automatic testing of different combinations
Expanded Text Ads (ETA)
A more controlled ad format with:
3 headlines
2 descriptions
Display URL with two path fields
Fixed layout with no dynamic combinations
Call-Only Ads
Designed to drive phone calls rather than website visits:
Business name
Phone number
Call-to-action text
Description lines
Verification URL (not shown to users)
Dynamic Search Ads (DSA)
Ads that use your website content to target relevant searches:
Dynamic headline generation based on search queries
Custom descriptions
Landing page selection based on website content
Requires website URL for crawling
Quality Analysis
Our comprehensive quality analysis evaluates your ads based on factors that influence Google's Quality Score:
Headline Analysis
Keyword Usage: Presence of keywords in headlines
Character Count: Optimal length for visibility
Power Words: Use of emotionally compelling words
Clarity: Clear communication of value proposition
Call to Action: Presence of action-oriented language
Description Analysis
Keyword Density: Optimal keyword usage
Benefit Focus: Clear articulation of benefits
Feature Inclusion: Mention of key features
Urgency Elements: Time-limited offers or scarcity
Call to Action: Clear next steps for the user
URL Path Analysis
Keyword Inclusion: Relevant keywords in display paths
Readability: Clear, understandable paths
Relevance: Connection to landing page content
Overall Ad Relevance
Keyword-to-Ad Relevance: Alignment between keywords and ad copy
Ad-to-Landing Page Relevance: Consistency across the user journey
Intent Match: Alignment with search intent
Performance Simulation
Our tool provides data-driven performance predictions based on:
Click-Through Rate (CTR) Prediction
Industry benchmarks
Ad quality factors
Keyword competition
Ad position estimates
Conversion Rate Prediction
Industry averages
Landing page quality
Offer strength
Call-to-action effectiveness
Cost Estimation
Keyword competition
Quality Score impact
Industry CPC averages
Budget allocation
ROI Calculation
Estimated clicks
Predicted conversions
Average conversion value
Cost projections
Best Practices
Our tool incorporates these Google Ads best practices:
Headline Best Practices
Include primary keywords in at least 2 headlines
Use numbers and statistics when relevant
Address user pain points directly
Include your unique selling proposition
Create a sense of urgency when appropriate
Keep headlines under 30 characters for full visibility
Use title case for better readability
Include at least one call-to-action headline
Description Best Practices
Include primary and secondary keywords naturally
Focus on benefits, not just features
Address objections proactively
Include specific offers or promotions
End with a clear call to action
Use all available character space (90 characters per description)
Maintain consistent messaging with headlines
Include trust signals (guarantees, social proof, etc.)
Extension Best Practices
Create at least 8 sitelinks for maximum visibility
Use callouts to highlight additional benefits
Include structured snippets relevant to your industry
Ensure extensions don't duplicate headline content
Make each extension unique and valuable
Use specific, action-oriented language
Keep sitelink text under 25 characters for mobile visibility
Ensure landing pages for sitelinks are relevant and optimized
Campaign Structure Best Practices
Group closely related keywords together
Create separate ad groups for different themes
Align ad copy closely with keywords in each ad group
Use a mix of match types for each keyword
Include negative keywords to prevent irrelevant clicks
Create separate campaigns for different goals or audiences
Set appropriate bid adjustments for devices, locations, and schedules
Implement conversion tracking for performance measurement
Export Options
The Google Ads Generator offers multiple export formats to fit your workflow:
CSV Format
Standard CSV format compatible with most spreadsheet applications
Includes all ad elements and extensions
Contains quality score and performance predictions
Suitable for analysis and record-keeping
Excel Format
Formatted Excel workbook with multiple sheets
Separate sheets for ads, extensions, and analysis
Includes charts and visualizations of predicted performance
Color-coded quality indicators
Google Ads Editor CSV
Specially formatted CSV for direct import into Google Ads Editor
Follows Google's required format specifications
Includes all necessary fields for campaign creation
Ready for immediate upload to Google Ads Editor
JSON Format
Structured data format for programmatic use
Complete ad data in machine-readable format
Suitable for integration with other marketing tools
Includes all metadata and analysis results
Advanced Features
Keyword Research Integration
Access to keyword volume data
Competition analysis
Cost-per-click estimates
Keyword difficulty scores
Seasonal trend information
Question-based keyword suggestions
Long-tail keyword recommendations
Competitor Analysis
Identify competitors bidding on similar keywords
Analyze competitor ad copy and messaging
Identify gaps and opportunities
Benchmark your ads against competitors
Receive suggestions for differentiation
Landing Page Suggestions
Alignment with ad messaging
Key elements to include
Conversion optimization tips
Mobile responsiveness recommendations
Page speed improvement suggestions
Call-to-action placement recommendations
Local Ad Optimization
Location extension suggestions
Local keyword recommendations
Geo-targeting strategies
Local offer suggestions
Community-focused messaging
Location-specific call-to-actions
Technical Details
System Requirements
Modern web browser (Chrome, Firefox, Safari, Edge)
Internet connection
Access to Alwrity AI Writer platform
Data Privacy
No permanent storage of business data
Secure processing of all inputs
Option to save ads to your account
Compliance with data protection regulations
API Integration
Available API endpoints for programmatic access
Documentation for developers
Rate limits and authentication requirements
Sample code for common use cases
FAQ
General Questions
Q: How accurate are the performance predictions? A: Performance predictions are based on industry benchmarks and Google's published data. While they provide a good estimate, actual performance may vary based on numerous factors including competition, seasonality, and market conditions.
Q: Can I edit the generated ads? A: Yes, all generated ads can be edited before export. You can modify headlines, descriptions, paths, and extensions to better fit your needs.
Q: How many ads can I generate? A: The tool allows unlimited ad generation within your Alwrity subscription limits.
Q: Are the generated ads compliant with Google's policies? A: The tool is designed to create policy-compliant ads, but we recommend reviewing Google's latest advertising policies as they may change over time.
Technical Questions
Q: Can I import my existing ads for optimization? A: Currently, the tool does not support importing existing ads, but this feature is on our roadmap.
Q: How do I import the exported files into Google Ads? A: For Google Ads Editor CSV files, open Google Ads Editor, go to File > Import, and select your exported file. For other formats, you may need to manually create campaigns using the generated content.
Q: Can I schedule automatic ad generation? A: Automated scheduling is not currently available but is planned for a future release.
Troubleshooting
Common Issues
Issue: Generated ads don't include my keywords Solution: Ensure your keywords are relevant to your business description and offerings. Try using more specific keywords or providing more detailed business information.
Issue: Quality score is consistently low Solution: Review the improvement suggestions in the Ad Performance tab. Common issues include keyword relevance, landing page alignment, and benefit clarity.
Issue: Export file isn't importing correctly into Google Ads Editor Solution: Ensure you're selecting the "Google Ads Editor CSV" export format. If problems persist, check for special characters in your ad copy that might be causing formatting issues.
Issue: Performance predictions seem unrealistic Solution: Adjust your industry selection and budget information to get more accurate predictions. Consider providing more specific audience targeting information.
Updates and Roadmap
Recent Updates
Added support for Performance Max campaign recommendations
Improved keyword research integration
Enhanced mobile ad optimization
Added 5 new industry templates
Improved quality score algorithm
Coming Soon
Competitor ad analysis tool
A/B testing performance simulator
Landing page builder integration
Automated ad scheduling recommendations
Video ad script generator
Google Shopping ad support
Multi-language ad generation
Custom template builder
Support
For additional help with the Google Ads Generator:
Visit our Help Center
Email support at support@example.com
Join our Community Forum
License
The Google Ads Generator is part of the Alwrity AI Writer platform and is subject to the platform's terms of service and licensing agreements.
Acknowledgments
Google Ads API documentation
Industry best practices from leading digital marketing experts
User feedback and feature requests
Last updated: [Current Date]
Version: 1.0.0

View File

@@ -0,0 +1,9 @@
"""
Google Ads Generator Module
This module provides functionality for generating high-converting Google Ads.
"""
from .google_ads_generator import write_google_ads
__all__ = ["write_google_ads"]

View File

@@ -0,0 +1,327 @@
"""
Ad Analyzer Module
This module provides functions for analyzing and scoring Google Ads.
"""
import re
from typing import Dict, List, Any, Tuple
import random
from urllib.parse import urlparse
def analyze_ad_quality(ad: Dict, primary_keywords: List[str], secondary_keywords: List[str],
business_name: str, call_to_action: str) -> Dict:
"""
Analyze the quality of a Google Ad based on best practices.
Args:
ad: Dictionary containing ad details
primary_keywords: List of primary keywords
secondary_keywords: List of secondary keywords
business_name: Name of the business
call_to_action: Call to action text
Returns:
Dictionary with analysis results
"""
# Initialize results
strengths = []
improvements = []
# Get ad components
headlines = ad.get("headlines", [])
descriptions = ad.get("descriptions", [])
path1 = ad.get("path1", "")
path2 = ad.get("path2", "")
# Check headline count
if len(headlines) >= 10:
strengths.append("Good number of headlines (10+) for optimization")
elif len(headlines) >= 5:
strengths.append("Adequate number of headlines for testing")
else:
improvements.append("Add more headlines (aim for 10+) to give Google's algorithm more options")
# Check description count
if len(descriptions) >= 4:
strengths.append("Good number of descriptions (4+) for optimization")
elif len(descriptions) >= 2:
strengths.append("Adequate number of descriptions for testing")
else:
improvements.append("Add more descriptions (aim for 4+) to give Google's algorithm more options")
# Check headline length
long_headlines = [h for h in headlines if len(h) > 30]
if long_headlines:
improvements.append(f"{len(long_headlines)} headline(s) exceed 30 characters and may be truncated")
else:
strengths.append("All headlines are within the recommended length")
# Check description length
long_descriptions = [d for d in descriptions if len(d) > 90]
if long_descriptions:
improvements.append(f"{len(long_descriptions)} description(s) exceed 90 characters and may be truncated")
else:
strengths.append("All descriptions are within the recommended length")
# Check keyword usage in headlines
headline_keywords = []
for kw in primary_keywords:
if any(kw.lower() in h.lower() for h in headlines):
headline_keywords.append(kw)
if len(headline_keywords) == len(primary_keywords):
strengths.append("All primary keywords are used in headlines")
elif headline_keywords:
strengths.append(f"{len(headline_keywords)} out of {len(primary_keywords)} primary keywords used in headlines")
missing_kw = [kw for kw in primary_keywords if kw not in headline_keywords]
improvements.append(f"Add these primary keywords to headlines: {', '.join(missing_kw)}")
else:
improvements.append("No primary keywords found in headlines - add keywords to improve relevance")
# Check keyword usage in descriptions
desc_keywords = []
for kw in primary_keywords:
if any(kw.lower() in d.lower() for d in descriptions):
desc_keywords.append(kw)
if len(desc_keywords) == len(primary_keywords):
strengths.append("All primary keywords are used in descriptions")
elif desc_keywords:
strengths.append(f"{len(desc_keywords)} out of {len(primary_keywords)} primary keywords used in descriptions")
missing_kw = [kw for kw in primary_keywords if kw not in desc_keywords]
improvements.append(f"Add these primary keywords to descriptions: {', '.join(missing_kw)}")
else:
improvements.append("No primary keywords found in descriptions - add keywords to improve relevance")
# Check for business name
if any(business_name.lower() in h.lower() for h in headlines):
strengths.append("Business name is included in headlines")
else:
improvements.append("Consider adding your business name to at least one headline")
# Check for call to action
if any(call_to_action.lower() in h.lower() for h in headlines) or any(call_to_action.lower() in d.lower() for d in descriptions):
strengths.append("Call to action is included in the ad")
else:
improvements.append(f"Add your call to action '{call_to_action}' to at least one headline or description")
# Check for numbers and statistics
has_numbers = any(bool(re.search(r'\d+', h)) for h in headlines) or any(bool(re.search(r'\d+', d)) for d in descriptions)
if has_numbers:
strengths.append("Ad includes numbers or statistics which can improve CTR")
else:
improvements.append("Consider adding numbers or statistics to increase credibility and CTR")
# Check for questions
has_questions = any('?' in h for h in headlines) or any('?' in d for d in descriptions)
if has_questions:
strengths.append("Ad includes questions which can engage users")
else:
improvements.append("Consider adding a question to engage users")
# Check for emotional triggers
emotional_words = ['you', 'free', 'because', 'instantly', 'new', 'save', 'proven', 'guarantee', 'love', 'discover']
has_emotional = any(any(word in h.lower() for word in emotional_words) for h in headlines) or \
any(any(word in d.lower() for word in emotional_words) for d in descriptions)
if has_emotional:
strengths.append("Ad includes emotional trigger words which can improve engagement")
else:
improvements.append("Consider adding emotional trigger words to increase engagement")
# Check for path relevance
if any(kw.lower() in path1.lower() or kw.lower() in path2.lower() for kw in primary_keywords):
strengths.append("Display URL paths include keywords which improves relevance")
else:
improvements.append("Add keywords to your display URL paths to improve relevance")
# Return the analysis results
return {
"strengths": strengths,
"improvements": improvements
}
def calculate_quality_score(ad: Dict, primary_keywords: List[str], landing_page: str, ad_type: str) -> Dict:
"""
Calculate a quality score for a Google Ad based on best practices.
Args:
ad: Dictionary containing ad details
primary_keywords: List of primary keywords
landing_page: Landing page URL
ad_type: Type of Google Ad
Returns:
Dictionary with quality score components
"""
# Initialize scores
keyword_relevance = 0
ad_relevance = 0
cta_effectiveness = 0
landing_page_relevance = 0
# Get ad components
headlines = ad.get("headlines", [])
descriptions = ad.get("descriptions", [])
path1 = ad.get("path1", "")
path2 = ad.get("path2", "")
# Calculate keyword relevance (0-10)
# Check if keywords are in headlines, descriptions, and paths
keyword_in_headline = sum(1 for kw in primary_keywords if any(kw.lower() in h.lower() for h in headlines))
keyword_in_description = sum(1 for kw in primary_keywords if any(kw.lower() in d.lower() for d in descriptions))
keyword_in_path = sum(1 for kw in primary_keywords if kw.lower() in path1.lower() or kw.lower() in path2.lower())
# Calculate score based on keyword presence
if len(primary_keywords) > 0:
headline_score = min(10, (keyword_in_headline / len(primary_keywords)) * 10)
description_score = min(10, (keyword_in_description / len(primary_keywords)) * 10)
path_score = min(10, (keyword_in_path / len(primary_keywords)) * 10)
# Weight the scores (headlines most important)
keyword_relevance = (headline_score * 0.6) + (description_score * 0.3) + (path_score * 0.1)
else:
keyword_relevance = 5 # Default score if no keywords provided
# Calculate ad relevance (0-10)
# Check for ad structure and content quality
# Check headline count and length
headline_count_score = min(10, (len(headlines) / 10) * 10) # Ideal: 10+ headlines
headline_length_score = 10 - min(10, (sum(1 for h in headlines if len(h) > 30) / max(1, len(headlines))) * 10)
# Check description count and length
description_count_score = min(10, (len(descriptions) / 4) * 10) # Ideal: 4+ descriptions
description_length_score = 10 - min(10, (sum(1 for d in descriptions if len(d) > 90) / max(1, len(descriptions))) * 10)
# Check for emotional triggers, questions, numbers
emotional_words = ['you', 'free', 'because', 'instantly', 'new', 'save', 'proven', 'guarantee', 'love', 'discover']
emotional_score = min(10, sum(1 for h in headlines if any(word in h.lower() for word in emotional_words)) +
sum(1 for d in descriptions if any(word in d.lower() for word in emotional_words)))
question_score = min(10, (sum(1 for h in headlines if '?' in h) + sum(1 for d in descriptions if '?' in d)) * 2)
number_score = min(10, (sum(1 for h in headlines if bool(re.search(r'\d+', h))) +
sum(1 for d in descriptions if bool(re.search(r'\d+', d)))) * 2)
# Calculate overall ad relevance score
ad_relevance = (headline_count_score * 0.15) + (headline_length_score * 0.15) + \
(description_count_score * 0.15) + (description_length_score * 0.15) + \
(emotional_score * 0.2) + (question_score * 0.1) + (number_score * 0.1)
# Calculate CTA effectiveness (0-10)
# Check for clear call to action
cta_phrases = ['get', 'buy', 'shop', 'order', 'sign up', 'register', 'download', 'learn', 'discover', 'find', 'call',
'contact', 'request', 'start', 'try', 'join', 'subscribe', 'book', 'schedule', 'apply']
cta_in_headline = any(any(phrase in h.lower() for phrase in cta_phrases) for h in headlines)
cta_in_description = any(any(phrase in d.lower() for phrase in cta_phrases) for d in descriptions)
if cta_in_headline and cta_in_description:
cta_effectiveness = 10
elif cta_in_headline:
cta_effectiveness = 8
elif cta_in_description:
cta_effectiveness = 7
else:
cta_effectiveness = 4
# Calculate landing page relevance (0-10)
# In a real implementation, this would analyze the landing page content
# For this example, we'll use a simplified approach
if landing_page:
# Check if domain seems relevant to keywords
domain = urlparse(landing_page).netloc
# Check if keywords are in the domain or path
keyword_in_url = any(kw.lower() in landing_page.lower() for kw in primary_keywords)
# Check if URL structure seems appropriate
has_https = landing_page.startswith('https://')
# Calculate landing page score
landing_page_relevance = 5 # Base score
if keyword_in_url:
landing_page_relevance += 3
if has_https:
landing_page_relevance += 2
# Cap at 10
landing_page_relevance = min(10, landing_page_relevance)
else:
landing_page_relevance = 5 # Default score if no landing page provided
# Calculate overall quality score (0-10)
overall_score = (keyword_relevance * 0.4) + (ad_relevance * 0.3) + (cta_effectiveness * 0.2) + (landing_page_relevance * 0.1)
# Calculate estimated CTR based on quality score
# This is a simplified model - in reality, CTR depends on many factors
base_ctr = {
"Responsive Search Ad": 3.17,
"Expanded Text Ad": 2.83,
"Call-Only Ad": 3.48,
"Dynamic Search Ad": 2.69
}.get(ad_type, 3.0)
# Adjust CTR based on quality score (±50%)
quality_factor = (overall_score - 5) / 5 # -1 to 1
estimated_ctr = base_ctr * (1 + (quality_factor * 0.5))
# Calculate estimated conversion rate
# Again, this is simplified - actual conversion rates depend on many factors
base_conversion_rate = 3.75 # Average conversion rate for search ads
# Adjust conversion rate based on quality score (±40%)
estimated_conversion_rate = base_conversion_rate * (1 + (quality_factor * 0.4))
# Return the quality score components
return {
"keyword_relevance": round(keyword_relevance, 1),
"ad_relevance": round(ad_relevance, 1),
"cta_effectiveness": round(cta_effectiveness, 1),
"landing_page_relevance": round(landing_page_relevance, 1),
"overall_score": round(overall_score, 1),
"estimated_ctr": round(estimated_ctr, 2),
"estimated_conversion_rate": round(estimated_conversion_rate, 2)
}
def analyze_keyword_relevance(keywords: List[str], ad_text: str) -> Dict:
"""
Analyze the relevance of keywords to ad text.
Args:
keywords: List of keywords to analyze
ad_text: Combined ad text (headlines and descriptions)
Returns:
Dictionary with keyword relevance analysis
"""
results = {}
for keyword in keywords:
# Check if keyword is in ad text
is_present = keyword.lower() in ad_text.lower()
# Check if keyword is in the first 100 characters
is_in_beginning = keyword.lower() in ad_text.lower()[:100]
# Count occurrences
occurrences = ad_text.lower().count(keyword.lower())
# Calculate density
density = (occurrences * len(keyword)) / len(ad_text) * 100 if len(ad_text) > 0 else 0
# Store results
results[keyword] = {
"present": is_present,
"in_beginning": is_in_beginning,
"occurrences": occurrences,
"density": round(density, 2),
"optimal_density": 0.5 <= density <= 2.5
}
return results

View File

@@ -0,0 +1,320 @@
"""
Ad Extensions Generator Module
This module provides functions for generating various types of Google Ads extensions.
"""
from typing import Dict, List, Any, Optional
import re
from ...gpt_providers.text_generation.main_text_generation import llm_text_gen
def generate_extensions(business_name: str, business_description: str, industry: str,
primary_keywords: List[str], unique_selling_points: List[str],
landing_page: str) -> Dict:
"""
Generate a complete set of ad extensions based on business information.
Args:
business_name: Name of the business
business_description: Description of the business
industry: Industry of the business
primary_keywords: List of primary keywords
unique_selling_points: List of unique selling points
landing_page: Landing page URL
Returns:
Dictionary with generated extensions
"""
# Generate sitelinks
sitelinks = generate_sitelinks(business_name, business_description, industry, primary_keywords, landing_page)
# Generate callouts
callouts = generate_callouts(business_name, unique_selling_points, industry)
# Generate structured snippets
snippets = generate_structured_snippets(business_name, business_description, industry, primary_keywords)
# Return all extensions
return {
"sitelinks": sitelinks,
"callouts": callouts,
"structured_snippets": snippets
}
def generate_sitelinks(business_name: str, business_description: str, industry: str,
primary_keywords: List[str], landing_page: str) -> List[Dict]:
"""
Generate sitelink extensions based on business information.
Args:
business_name: Name of the business
business_description: Description of the business
industry: Industry of the business
primary_keywords: List of primary keywords
landing_page: Landing page URL
Returns:
List of dictionaries with sitelink information
"""
# Define common sitelink types by industry
industry_sitelinks = {
"E-commerce": ["Shop Now", "Best Sellers", "New Arrivals", "Sale Items", "Customer Reviews", "About Us"],
"SaaS/Technology": ["Features", "Pricing", "Demo", "Case Studies", "Support", "Blog"],
"Healthcare": ["Services", "Locations", "Providers", "Insurance", "Patient Portal", "Contact Us"],
"Education": ["Programs", "Admissions", "Campus", "Faculty", "Student Life", "Apply Now"],
"Finance": ["Services", "Rates", "Calculators", "Locations", "Apply Now", "About Us"],
"Real Estate": ["Listings", "Sell Your Home", "Neighborhoods", "Agents", "Mortgage", "Contact Us"],
"Legal": ["Practice Areas", "Attorneys", "Results", "Testimonials", "Free Consultation", "Contact"],
"Travel": ["Destinations", "Deals", "Book Now", "Reviews", "FAQ", "Contact Us"],
"Food & Beverage": ["Menu", "Locations", "Order Online", "Reservations", "Catering", "About Us"]
}
# Get sitelinks for the specified industry, or use default
sitelink_types = industry_sitelinks.get(industry, ["About Us", "Services", "Products", "Contact Us", "Testimonials", "FAQ"])
# Generate sitelinks
sitelinks = []
base_url = landing_page.rstrip('/') if landing_page else ""
for sitelink_type in sitelink_types:
# Generate URL path based on sitelink type
path = sitelink_type.lower().replace(' ', '-')
url = f"{base_url}/{path}" if base_url else f"https://example.com/{path}"
# Generate description based on sitelink type
description = ""
if sitelink_type == "About Us":
description = f"Learn more about {business_name} and our mission."
elif sitelink_type == "Services" or sitelink_type == "Products":
description = f"Explore our range of {primary_keywords[0] if primary_keywords else 'offerings'}."
elif sitelink_type == "Contact Us":
description = f"Get in touch with our team for assistance."
elif sitelink_type == "Testimonials" or sitelink_type == "Reviews":
description = f"See what our customers say about us."
elif sitelink_type == "FAQ":
description = f"Find answers to common questions."
elif sitelink_type == "Pricing" or sitelink_type == "Rates":
description = f"View our competitive pricing options."
elif sitelink_type == "Shop Now" or sitelink_type == "Order Online":
description = f"Browse and purchase our {primary_keywords[0] if primary_keywords else 'products'} online."
# Add the sitelink
sitelinks.append({
"text": sitelink_type,
"url": url,
"description": description
})
return sitelinks
def generate_callouts(business_name: str, unique_selling_points: List[str], industry: str) -> List[str]:
"""
Generate callout extensions based on business information.
Args:
business_name: Name of the business
unique_selling_points: List of unique selling points
industry: Industry of the business
Returns:
List of callout texts
"""
# Use provided USPs if available
if unique_selling_points and len(unique_selling_points) >= 4:
# Ensure callouts are not too long (25 characters max)
callouts = []
for usp in unique_selling_points:
if len(usp) <= 25:
callouts.append(usp)
else:
# Try to truncate at a space
truncated = usp[:22] + "..."
callouts.append(truncated)
return callouts[:8] # Return up to 8 callouts
# Define common callouts by industry
industry_callouts = {
"E-commerce": ["Free Shipping", "24/7 Customer Service", "Secure Checkout", "Easy Returns", "Price Match Guarantee", "Next Day Delivery", "Satisfaction Guaranteed", "Exclusive Deals"],
"SaaS/Technology": ["24/7 Support", "Free Trial", "No Credit Card Required", "Easy Integration", "Data Security", "Cloud-Based", "Regular Updates", "Customizable"],
"Healthcare": ["Board Certified", "Most Insurance Accepted", "Same-Day Appointments", "Compassionate Care", "State-of-the-Art Facility", "Experienced Staff", "Convenient Location", "Telehealth Available"],
"Education": ["Accredited Programs", "Expert Faculty", "Financial Aid", "Career Services", "Small Class Sizes", "Flexible Schedule", "Online Options", "Hands-On Learning"],
"Finance": ["FDIC Insured", "No Hidden Fees", "Personalized Service", "Online Banking", "Mobile App", "Low Interest Rates", "Financial Planning", "Retirement Services"],
"Real Estate": ["Free Home Valuation", "Virtual Tours", "Experienced Agents", "Local Expertise", "Financing Available", "Property Management", "Commercial & Residential", "Investment Properties"],
"Legal": ["Free Consultation", "No Win No Fee", "Experienced Attorneys", "24/7 Availability", "Proven Results", "Personalized Service", "Multiple Practice Areas", "Aggressive Representation"]
}
# Get callouts for the specified industry, or use default
callouts = industry_callouts.get(industry, ["Professional Service", "Experienced Team", "Customer Satisfaction", "Quality Guaranteed", "Competitive Pricing", "Fast Service", "Personalized Solutions", "Trusted Provider"])
return callouts
def generate_structured_snippets(business_name: str, business_description: str, industry: str, primary_keywords: List[str]) -> Dict:
"""
Generate structured snippet extensions based on business information.
Args:
business_name: Name of the business
business_description: Description of the business
industry: Industry of the business
primary_keywords: List of primary keywords
Returns:
Dictionary with structured snippet information
"""
# Define common snippet headers and values by industry
industry_snippets = {
"E-commerce": {
"header": "Brands",
"values": ["Nike", "Adidas", "Apple", "Samsung", "Sony", "LG", "Dell", "HP"]
},
"SaaS/Technology": {
"header": "Services",
"values": ["Cloud Storage", "Data Analytics", "CRM", "Project Management", "Email Marketing", "Cybersecurity", "API Integration", "Automation"]
},
"Healthcare": {
"header": "Services",
"values": ["Preventive Care", "Diagnostics", "Treatment", "Surgery", "Rehabilitation", "Counseling", "Telemedicine", "Wellness Programs"]
},
"Education": {
"header": "Courses",
"values": ["Business", "Technology", "Healthcare", "Design", "Engineering", "Education", "Arts", "Sciences"]
},
"Finance": {
"header": "Services",
"values": ["Checking Accounts", "Savings Accounts", "Loans", "Mortgages", "Investments", "Retirement Planning", "Insurance", "Wealth Management"]
},
"Real Estate": {
"header": "Types",
"values": ["Single-Family Homes", "Condos", "Townhouses", "Apartments", "Commercial", "Land", "New Construction", "Luxury Homes"]
},
"Legal": {
"header": "Services",
"values": ["Personal Injury", "Family Law", "Criminal Defense", "Estate Planning", "Business Law", "Immigration", "Real Estate Law", "Intellectual Property"]
}
}
# Get snippets for the specified industry, or use default
snippet_info = industry_snippets.get(industry, {
"header": "Services",
"values": ["Consultation", "Assessment", "Implementation", "Support", "Maintenance", "Training", "Customization", "Analysis"]
})
# If we have primary keywords, try to incorporate them
if primary_keywords:
# Try to determine a better header based on keywords
service_keywords = ["service", "support", "consultation", "assistance", "help"]
product_keywords = ["product", "item", "good", "merchandise"]
brand_keywords = ["brand", "make", "manufacturer"]
for kw in primary_keywords:
kw_lower = kw.lower()
if any(service_word in kw_lower for service_word in service_keywords):
snippet_info["header"] = "Services"
break
elif any(product_word in kw_lower for product_word in product_keywords):
snippet_info["header"] = "Products"
break
elif any(brand_word in kw_lower for brand_word in brand_keywords):
snippet_info["header"] = "Brands"
break
return snippet_info
def generate_custom_extensions(business_info: Dict, extension_type: str) -> Any:
"""
Generate custom extensions using AI based on business information.
Args:
business_info: Dictionary with business information
extension_type: Type of extension to generate
Returns:
Generated extension data
"""
# Extract business information
business_name = business_info.get("business_name", "")
business_description = business_info.get("business_description", "")
industry = business_info.get("industry", "")
primary_keywords = business_info.get("primary_keywords", [])
unique_selling_points = business_info.get("unique_selling_points", [])
# Create a prompt based on extension type
if extension_type == "sitelinks":
prompt = f"""
Generate 6 sitelink extensions for a Google Ads campaign for the following business:
Business Name: {business_name}
Business Description: {business_description}
Industry: {industry}
Keywords: {', '.join(primary_keywords)}
For each sitelink, provide:
1. Link text (max 25 characters)
2. Description line 1 (max 35 characters)
3. Description line 2 (max 35 characters)
Format the response as a JSON array of objects with "text", "description1", and "description2" fields.
"""
elif extension_type == "callouts":
prompt = f"""
Generate 8 callout extensions for a Google Ads campaign for the following business:
Business Name: {business_name}
Business Description: {business_description}
Industry: {industry}
Keywords: {', '.join(primary_keywords)}
Unique Selling Points: {', '.join(unique_selling_points)}
Each callout should:
1. Be 25 characters or less
2. Highlight a feature, benefit, or unique selling point
3. Be concise and impactful
Format the response as a JSON array of strings.
"""
elif extension_type == "structured_snippets":
prompt = f"""
Generate structured snippet extensions for a Google Ads campaign for the following business:
Business Name: {business_name}
Business Description: {business_description}
Industry: {industry}
Keywords: {', '.join(primary_keywords)}
Provide:
1. The most appropriate header type (e.g., Brands, Services, Products, Courses, etc.)
2. 8 values that are relevant to the business (each 25 characters or less)
Format the response as a JSON object with "header" and "values" fields.
"""
else:
return None
# Generate the extensions using the LLM
try:
response = llm_text_gen(prompt)
# Process the response based on extension type
# In a real implementation, you would parse the JSON response
# For this example, we'll return a placeholder
if extension_type == "sitelinks":
return [
{"text": "About Us", "description1": "Learn about our company", "description2": "Our history and mission"},
{"text": "Services", "description1": "Explore our service offerings", "description2": "Solutions for your needs"},
{"text": "Products", "description1": "Browse our product catalog", "description2": "Quality items at great prices"},
{"text": "Contact Us", "description1": "Get in touch with our team", "description2": "We're here to help you"},
{"text": "Testimonials", "description1": "See what customers say", "description2": "Real reviews from real people"},
{"text": "FAQ", "description1": "Frequently asked questions", "description2": "Find quick answers here"}
]
elif extension_type == "callouts":
return ["Free Shipping", "24/7 Support", "Money-Back Guarantee", "Expert Team", "Premium Quality", "Fast Service", "Affordable Prices", "Satisfaction Guaranteed"]
elif extension_type == "structured_snippets":
return {"header": "Services", "values": ["Consultation", "Installation", "Maintenance", "Repair", "Training", "Support", "Design", "Analysis"]}
else:
return None
except Exception as e:
print(f"Error generating extensions: {str(e)}")
return None

View File

@@ -0,0 +1,219 @@
"""
Ad Templates Module
This module provides templates for different ad types and industries.
"""
from typing import Dict, List, Any
def get_industry_templates(industry: str) -> Dict:
"""
Get ad templates specific to an industry.
Args:
industry: The industry to get templates for
Returns:
Dictionary with industry-specific templates
"""
# Define templates for different industries
templates = {
"E-commerce": {
"headline_templates": [
"{product} - {benefit} | {business_name}",
"Shop {product} - {discount} Off Today",
"Top-Rated {product} - Free Shipping",
"{benefit} with Our {product}",
"New {product} Collection - {benefit}",
"{discount}% Off {product} - Limited Time",
"Buy {product} Online - Fast Delivery",
"{product} Sale Ends {timeframe}",
"Best-Selling {product} from {business_name}",
"Premium {product} - {benefit}"
],
"description_templates": [
"Shop our selection of {product} and enjoy {benefit}. Free shipping on orders over ${amount}. Order now!",
"Looking for quality {product}? Get {benefit} with our {product}. {discount} off your first order!",
"{business_name} offers premium {product} with {benefit}. Shop online or visit our store today!",
"Discover our {product} collection. {benefit} guaranteed or your money back. Order now and save {discount}!"
],
"emotional_triggers": ["exclusive", "limited time", "sale", "discount", "free shipping", "bestseller", "new arrival"],
"call_to_actions": ["Shop Now", "Buy Today", "Order Online", "Get Yours", "Add to Cart", "Save Today"]
},
"SaaS/Technology": {
"headline_templates": [
"{product} Software - {benefit}",
"Try {product} Free for {timeframe}",
"{benefit} with Our {product} Platform",
"{product} - Rated #1 for {feature}",
"New {feature} in Our {product} Software",
"{business_name} - {benefit} Software",
"Streamline {pain_point} with {product}",
"{product} Software - {discount} Off",
"Enterprise-Grade {product} for {audience}",
"{product} - {benefit} Guaranteed"
],
"description_templates": [
"{business_name}'s {product} helps you {benefit}. Try it free for {timeframe}. No credit card required.",
"Struggling with {pain_point}? Our {product} provides {benefit}. Join {number}+ satisfied customers.",
"Our {product} platform offers {feature} to help you {benefit}. Rated {rating}/5 by {source}.",
"{product} by {business_name}: {benefit} for your business. Plans starting at ${price}/month."
],
"emotional_triggers": ["efficient", "time-saving", "seamless", "integrated", "secure", "scalable", "innovative"],
"call_to_actions": ["Start Free Trial", "Request Demo", "Learn More", "Sign Up Free", "Get Started", "See Plans"]
},
"Healthcare": {
"headline_templates": [
"{service} in {location} | {business_name}",
"Expert {service} - {benefit}",
"Quality {service} for {audience}",
"{business_name} - {credential} {professionals}",
"Same-Day {service} Appointments",
"{service} Specialists in {location}",
"Affordable {service} - {benefit}",
"{symptom}? Get {service} Today",
"Advanced {service} Technology",
"Compassionate {service} Care"
],
"description_templates": [
"{business_name} provides expert {service} with {benefit}. Our {credential} team is ready to help. Schedule today!",
"Experiencing {symptom}? Our {professionals} offer {service} with {benefit}. Most insurance accepted.",
"Quality {service} in {location}. {benefit} from our experienced team. Call now to schedule your appointment.",
"Our {service} center provides {benefit} for {audience}. Open {days} with convenient hours."
],
"emotional_triggers": ["trusted", "experienced", "compassionate", "advanced", "personalized", "comprehensive", "gentle"],
"call_to_actions": ["Schedule Now", "Book Appointment", "Call Today", "Free Consultation", "Learn More", "Find Relief"]
},
"Real Estate": {
"headline_templates": [
"{property_type} in {location} | {business_name}",
"{property_type} for {price_range} - {location}",
"Find Your Dream {property_type} in {location}",
"{feature} {property_type} - {location}",
"New {property_type} Listings in {location}",
"Sell Your {property_type} in {timeframe}",
"{business_name} - {credential} {professionals}",
"{property_type} {benefit} - {location}",
"Exclusive {property_type} Listings",
"{number}+ {property_type} Available Now"
],
"description_templates": [
"Looking for {property_type} in {location}? {business_name} offers {benefit}. Browse our listings or call us today!",
"Sell your {property_type} in {location} with {business_name}. Our {professionals} provide {benefit}. Free valuation!",
"{business_name}: {credential} {professionals} helping you find the perfect {property_type} in {location}. Call now!",
"Discover {feature} {property_type} in {location}. Prices from {price_range}. Schedule a viewing today!"
],
"emotional_triggers": ["dream home", "exclusive", "luxury", "investment", "perfect location", "spacious", "modern"],
"call_to_actions": ["View Listings", "Schedule Viewing", "Free Valuation", "Call Now", "Learn More", "Get Pre-Approved"]
}
}
# Return templates for the specified industry, or a default if not found
return templates.get(industry, {
"headline_templates": [
"{product/service} - {benefit} | {business_name}",
"Professional {product/service} - {benefit}",
"{benefit} with Our {product/service}",
"{business_name} - {credential} {product/service}",
"Quality {product/service} for {audience}",
"Affordable {product/service} - {benefit}",
"{product/service} in {location}",
"{feature} {product/service} by {business_name}",
"Experienced {product/service} Provider",
"{product/service} - Satisfaction Guaranteed"
],
"description_templates": [
"{business_name} offers professional {product/service} with {benefit}. Contact us today to learn more!",
"Looking for quality {product/service}? {business_name} provides {benefit}. Call now for more information.",
"Our {product/service} helps you {benefit}. Trusted by {number}+ customers. Contact us today!",
"{business_name}: {credential} {product/service} provider. We offer {benefit} for {audience}. Learn more!"
],
"emotional_triggers": ["professional", "quality", "trusted", "experienced", "affordable", "reliable", "satisfaction"],
"call_to_actions": ["Contact Us", "Learn More", "Call Now", "Get Quote", "Visit Website", "Schedule Consultation"]
})
def get_ad_type_templates(ad_type: str) -> Dict:
"""
Get templates specific to an ad type.
Args:
ad_type: The ad type to get templates for
Returns:
Dictionary with ad type-specific templates
"""
# Define templates for different ad types
templates = {
"Responsive Search Ad": {
"headline_count": 15,
"description_count": 4,
"headline_max_length": 30,
"description_max_length": 90,
"best_practices": [
"Include at least 3 headlines with keywords",
"Create headlines with different lengths",
"Include at least 1 headline with a call to action",
"Include at least 1 headline with your brand name",
"Create descriptions that complement each other",
"Include keywords in at least 2 descriptions",
"Include a call to action in at least 1 description"
]
},
"Expanded Text Ad": {
"headline_count": 3,
"description_count": 2,
"headline_max_length": 30,
"description_max_length": 90,
"best_practices": [
"Include keywords in Headline 1",
"Use a call to action in Headline 2 or 3",
"Include your brand name in one headline",
"Make descriptions complementary but able to stand alone",
"Include keywords in at least one description",
"Include a call to action in at least one description"
]
},
"Call-Only Ad": {
"headline_count": 2,
"description_count": 2,
"headline_max_length": 30,
"description_max_length": 90,
"best_practices": [
"Focus on encouraging phone calls",
"Include language like 'Call now', 'Speak to an expert', etc.",
"Mention phone availability (e.g., '24/7', 'Available now')",
"Include benefits of calling rather than clicking",
"Be clear about who will answer the call",
"Include any special offers for callers"
]
},
"Dynamic Search Ad": {
"headline_count": 0, # Headlines are dynamically generated
"description_count": 2,
"headline_max_length": 0, # N/A
"description_max_length": 90,
"best_practices": [
"Create descriptions that work with any dynamically generated headline",
"Focus on your unique selling points",
"Include a strong call to action",
"Highlight benefits that apply across your product/service range",
"Avoid specific product mentions that might not match the dynamic headline"
]
}
}
# Return templates for the specified ad type, or a default if not found
return templates.get(ad_type, {
"headline_count": 3,
"description_count": 2,
"headline_max_length": 30,
"description_max_length": 90,
"best_practices": [
"Include keywords in headlines",
"Use a call to action",
"Include your brand name",
"Make descriptions informative and compelling",
"Include keywords in descriptions",
"Highlight unique selling points"
]
})

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ well-researched FAQs from various content sources with customizable options.
import sys
import json
import re
from typing import Dict, List, Optional, Union
from pathlib import Path
from enum import Enum
@@ -15,12 +16,12 @@ from loguru import logger
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
from lib.ai_web_researcher.google_serp_search import google_search
from lib.ai_web_researcher.tavily_ai_search import tavily_search
from lib.ai_web_researcher.tavily_ai_search import do_tavily_ai_search
from lib.ai_web_researcher.metaphor_basic_neural_web_search import metaphor_search_articles
logger.remove()
logger.add(sys.stdout,
colorize=True,
colorize=True,
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}")
class TargetAudience(Enum):
@@ -51,6 +52,7 @@ class FAQConfig:
time_range: str = "last_6_months"
exclude_domains: List[str] = None
language: str = "English"
selected_search_queries: List[str] = None
@dataclass
class FAQItem:
@@ -71,26 +73,77 @@ class FAQGenerator:
self.config = config or FAQConfig()
self.faqs: List[FAQItem] = []
self.research_results = {}
self.search_queries = []
async def generate_faqs(self, content: str, content_type: str = "general") -> List[FAQItem]:
def generate_search_queries(self, content: str) -> List[str]:
"""Generate search queries based on the content."""
try:
prompt = f"""Based on the following content, generate 5 specific search queries that would help create comprehensive FAQs.
Content: {content}
Guidelines for search queries:
1. Focus on key concepts and terms
2. Include common questions users might have
3. Cover technical aspects that need clarification
4. Include best practices and recommendations
5. Make queries specific and focused
Please provide exactly 5 search queries, one per line.
Do not include numbers or bullet points in the queries.
"""
response = llm_text_gen(prompt)
# Clean up the queries by removing numbers and extra spaces
queries = []
for line in response.split('\n'):
# Remove any leading numbers, dots, or spaces
cleaned = re.sub(r'^\d+\.\s*', '', line.strip())
if cleaned:
queries.append(cleaned)
self.search_queries = queries[:5] # Ensure we only get 5 queries
return self.search_queries
except Exception as err:
logger.error(f"Failed to generate search queries: {err}")
return []
def _clean_search_query(self, query: str) -> str:
"""Clean up a search query by removing numbers and extra formatting."""
# Remove any leading numbers, dots, or spaces
cleaned = re.sub(r'^\d+\.\s*', '', query.strip())
# Remove any quotes
cleaned = cleaned.replace('"', '').replace("'", '')
# Remove any extra spaces
cleaned = ' '.join(cleaned.split())
return cleaned
def generate_faqs(self, content: str, content_type: str = "general") -> List[FAQItem]:
"""Generate FAQs from the given content with research integration."""
try:
# Step 1: Research the topic
research_results = await self._conduct_research(content)
if not self.config.selected_search_queries:
raise ValueError("No search queries selected. Please select queries to proceed.")
# Clean up selected queries
cleaned_queries = [self._clean_search_query(q) for q in self.config.selected_search_queries]
self.config.selected_search_queries = cleaned_queries
# Step 1: Research the topic using selected queries
research_results = self._conduct_research(content)
# Step 2: Generate initial FAQs
initial_faqs = await self._generate_initial_faqs(content, research_results)
initial_faqs = self._generate_initial_faqs(content, research_results)
# Step 3: Enhance FAQs with research
enhanced_faqs = await self._enhance_faqs_with_research(initial_faqs, research_results)
enhanced_faqs = self._enhance_faqs_with_research(initial_faqs, research_results)
# Step 4: Add code examples if requested
if self.config.include_code_examples:
enhanced_faqs = await self._add_code_examples(enhanced_faqs)
enhanced_faqs = self._add_code_examples(enhanced_faqs)
# Step 5: Add references if requested
if self.config.include_references:
enhanced_faqs = await self._add_references(enhanced_faqs, research_results)
enhanced_faqs = self._add_references(enhanced_faqs, research_results)
self.faqs = enhanced_faqs
return enhanced_faqs
@@ -99,38 +152,34 @@ class FAQGenerator:
logger.error(f"Failed to generate FAQs: {err}")
raise
async def _conduct_research(self, content: str) -> Dict:
"""Conduct online research based on the content."""
def _conduct_research(self, content: str) -> Dict:
"""Conduct online research based on the selected search queries."""
try:
research_prompt = f"""Based on the following content, identify key topics and questions for research:
{content}
Please provide a list of research topics and questions that would help create comprehensive FAQs.
Focus on:
1. Key concepts and terms
2. Common questions users might have
3. Technical aspects that need clarification
4. Best practices and recommendations
"""
research_topics = await llm_text_gen(research_prompt)
# Conduct research for each topic
research_results = {}
for topic in research_topics.split('\n'):
if topic.strip():
for query in self.config.selected_search_queries:
try:
# Clean the query before searching
cleaned_query = self._clean_search_query(query)
logger.info(f"Researching query: {cleaned_query}")
# Select search function based on search depth
if self.config.search_depth == SearchDepth.BASIC:
results = await google_search(topic.strip())
results = google_search(cleaned_query)
elif self.config.search_depth == SearchDepth.COMPREHENSIVE:
results = await tavily_search(topic.strip())
results = do_tavily_ai_search(cleaned_query)
elif self.config.search_depth == SearchDepth.EXPERT:
results = await metaphor_search_articles(topic.strip())
results = metaphor_search_articles(cleaned_query)
else:
logger.warning(f"Unknown search depth: {self.config.search_depth}, defaulting to Google search")
results = await google_search(topic.strip())
results = google_search(cleaned_query)
research_results[topic.strip()] = results
research_results[query] = results
logger.info(f"Research completed for query: {query}")
except Exception as err:
logger.error(f"Failed to research query '{query}': {err}")
continue
return research_results
@@ -138,7 +187,7 @@ class FAQGenerator:
logger.error(f"Failed to conduct research: {err}")
return {}
async def _generate_initial_faqs(self, content: str, research_results: Dict) -> List[FAQItem]:
def _generate_initial_faqs(self, content: str, research_results: Dict) -> List[FAQItem]:
"""Generate initial FAQs using LLM."""
try:
system_prompt = f"""You are an expert FAQ generator with deep knowledge in content creation and technical writing.
@@ -159,6 +208,13 @@ class FAQGenerator:
- Based on the provided research
- Relevant to the target audience
- Written in the specified style
Format each FAQ exactly as follows:
Q: [Your question here]
A: [Your detailed answer here]
Category: [Category name]
Confidence: [Score between 0 and 1]
---
"""
prompt = f"""Content to generate FAQs from:
@@ -168,22 +224,26 @@ class FAQGenerator:
{json.dumps(research_results, indent=2)}
Please generate {self.config.num_faqs} FAQs following the guidelines above.
Format each FAQ with:
- Question
- Detailed answer
- Category
- Confidence score (0-1)
Each FAQ must be separated by '---' and include all required fields.
"""
response = await llm_text_gen(prompt, system_prompt=system_prompt)
response = llm_text_gen(prompt, system_prompt=system_prompt)
logger.info(f"LLM Response: {response}")
# Parse the response into FAQItem objects
faqs = []
current_faq = None
for line in response.split('\n'):
line = line.strip()
if not line or line == '---':
if current_faq and current_faq.question and current_faq.answer:
faqs.append(current_faq)
current_faq = None
continue
if line.startswith('Q:'):
if current_faq:
if current_faq and current_faq.question and current_faq.answer:
faqs.append(current_faq)
current_faq = FAQItem(question=line[2:].strip(), answer="", category="")
elif line.startswith('A:'):
@@ -194,18 +254,23 @@ class FAQGenerator:
current_faq.category = line[9:].strip()
elif line.startswith('Confidence:'):
if current_faq:
current_faq.confidence_score = float(line[11:].strip())
try:
current_faq.confidence_score = float(line[11:].strip())
except ValueError:
current_faq.confidence_score = 0.5
if current_faq:
# Add the last FAQ if it exists and is complete
if current_faq and current_faq.question and current_faq.answer:
faqs.append(current_faq)
logger.info(f"Generated {len(faqs)} FAQs")
return faqs
except Exception as err:
logger.error(f"Failed to generate initial FAQs: {err}")
raise
async def _enhance_faqs_with_research(self, faqs: List[FAQItem], research_results: Dict) -> List[FAQItem]:
def _enhance_faqs_with_research(self, faqs: List[FAQItem], research_results: Dict) -> List[FAQItem]:
"""Enhance FAQs with research findings."""
try:
enhanced_faqs = []
@@ -231,7 +296,7 @@ class FAQGenerator:
4. Keeping the answer concise and clear
"""
enhanced_answer = await llm_text_gen(enhancement_prompt)
enhanced_answer = llm_text_gen(enhancement_prompt)
faq.answer = enhanced_answer
enhanced_faqs.append(faq)
@@ -242,24 +307,20 @@ class FAQGenerator:
logger.error(f"Failed to enhance FAQs with research: {err}")
return faqs
async def _add_code_examples(self, faqs: List[FAQItem]) -> List[FAQItem]:
def _add_code_examples(self, faqs: List[FAQItem]) -> List[FAQItem]:
"""Add code examples to FAQs where applicable."""
try:
for faq in faqs:
if self._is_technical_question(faq.question):
code_prompt = f"""Generate a code example for the following FAQ:
Question: {faq.question}
Answer: {faq.answer}
Please provide a relevant code example that:
1. Illustrates the answer clearly
2. Includes comments and explanations
3. Follows best practices
4. Is easy to understand
Please provide a relevant code example that demonstrates the concept.
Include comments and explanations where necessary.
"""
code_example = await llm_text_gen(code_prompt)
code_example = llm_text_gen(code_prompt)
faq.code_example = code_example
return faqs
@@ -268,21 +329,19 @@ class FAQGenerator:
logger.error(f"Failed to add code examples: {err}")
return faqs
async def _add_references(self, faqs: List[FAQItem], research_results: Dict) -> List[FAQItem]:
"""Add references to FAQs."""
def _add_references(self, faqs: List[FAQItem], research_results: Dict) -> List[FAQItem]:
"""Add references to FAQs based on research results."""
try:
for faq in faqs:
relevant_research = self._find_relevant_research(faq, research_results)
if relevant_research:
faq.references = [
{
"title": ref.get("title", ""),
"url": ref.get("url", ""),
"source": ref.get("source", ""),
"date": ref.get("date", "")
}
for ref in relevant_research.get("references", [])
]
references = []
for source, content in relevant_research.items():
references.append({
"source": source,
"content": content
})
faq.references = references
return faqs
@@ -291,8 +350,7 @@ class FAQGenerator:
return faqs
def _find_relevant_research(self, faq: FAQItem, research_results: Dict) -> Dict:
"""Find research relevant to a specific FAQ."""
# Simple keyword matching for now - can be enhanced with semantic search
"""Find research results relevant to a specific FAQ."""
relevant_research = {}
for topic, results in research_results.items():
if any(keyword in faq.question.lower() for keyword in topic.lower().split()):
@@ -308,8 +366,8 @@ class FAQGenerator:
"""Convert FAQs to markdown format."""
markdown = "# Frequently Asked Questions\n\n"
for i, faq in enumerate(self.faqs, 1):
markdown += f"## {i}. {faq.question}\n\n"
for faq in self.faqs:
markdown += f"## {faq.question}\n\n"
markdown += f"{faq.answer}\n\n"
if faq.code_example:
@@ -320,7 +378,7 @@ class FAQGenerator:
if faq.references:
markdown += "### References\n"
for ref in faq.references:
markdown += f"- [{ref['title']}]({ref['url']}) - {ref['source']} ({ref['date']})\n"
markdown += f"- {ref['source']}\n"
markdown += "\n"
return markdown
@@ -333,52 +391,52 @@ class FAQGenerator:
<head>
<title>Frequently Asked Questions</title>
<style>
.faq-container { max-width: 800px; margin: 0 auto; }
.faq-item { margin-bottom: 2em; }
.question { font-weight: bold; font-size: 1.2em; }
.answer { margin: 1em 0; }
.code-example { background: #f5f5f5; padding: 1em; }
.references { margin-top: 1em; font-size: 0.9em; }
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.faq { margin-bottom: 30px; }
.question { font-weight: bold; font-size: 1.2em; color: #2c3e50; }
.answer { margin: 10px 0; }
.code-example { background: #f8f9fa; padding: 15px; border-radius: 4px; }
.references { margin-top: 15px; font-size: 0.9em; }
</style>
</head>
<body>
<div class="faq-container">
<h1>Frequently Asked Questions</h1>
<h1>Frequently Asked Questions</h1>
"""
for i, faq in enumerate(self.faqs, 1):
for faq in self.faqs:
html += f"""
<div class="faq-item">
<div class="question">{i}. {faq.question}</div>
<div class="answer">{faq.answer}</div>
<div class="faq">
<div class="question">{faq.question}</div>
<div class="answer">{faq.answer}</div>
"""
if faq.code_example:
html += f"""
<pre class="code-example">{faq.code_example}</pre>
<div class="code-example">
<pre><code>{faq.code_example}</code></pre>
</div>
"""
if faq.references:
html += """
<div class="references">
<h3>References</h3>
<ul>
<div class="references">
<h3>References</h3>
<ul>
"""
for ref in faq.references:
html += f"""
<li><a href="{ref['url']}">{ref['title']}</a> - {ref['source']} ({ref['date']})</li>
<li>{ref['source']}</li>
"""
html += """
</ul>
</div>
</ul>
</div>
"""
html += """
</div>
</div>
"""
html += """
</div>
</body>
</html>
"""

View File

@@ -5,15 +5,27 @@ This module provides a user-friendly interface for generating FAQs from various
"""
import streamlit as st
import asyncio
from pathlib import Path
from typing import Optional
import json
import requests
from bs4 import BeautifulSoup
import logging
import pyperclip
from .faqs_generator_blog import FAQGenerator, FAQConfig, TargetAudience, FAQStyle, SearchDepth
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def copy_to_clipboard(text: str) -> None:
"""Copy text to clipboard and show success message."""
try:
pyperclip.copy(text)
st.success("Copied to clipboard!")
except Exception as e:
st.error(f"Failed to copy to clipboard: {str(e)}")
def fetch_url_content(url):
"""Fetch and extract content from a URL."""
@@ -42,15 +54,27 @@ def fetch_url_content(url):
return None
def main():
st.set_page_config(
page_title="FAQ Generator",
page_icon="",
layout="wide"
)
st.title("FAQ Generator")
st.markdown("Generate comprehensive FAQs from your content with research integration.")
# Initialize session state variables if they don't exist
if 'search_queries' not in st.session_state:
st.session_state.search_queries = []
if 'selected_queries' not in st.session_state:
st.session_state.selected_queries = []
if 'research_completed' not in st.session_state:
st.session_state.research_completed = False
if 'research_results' not in st.session_state:
st.session_state.research_results = {}
if 'faq_config' not in st.session_state:
st.session_state.faq_config = None
if 'generator' not in st.session_state:
st.session_state.generator = FAQGenerator()
if 'generated_faqs' not in st.session_state:
st.session_state.generated_faqs = None
if 'output_format' not in st.session_state:
st.session_state.output_format = "Preview"
# Sidebar for configuration
with st.sidebar:
st.header("Configuration")
@@ -99,40 +123,137 @@ def main():
if content:
st.text_area("Extracted Content", content, height=300)
# Generate button
if st.button("Generate FAQs") and content:
try:
# Create config
config = FAQConfig(
num_faqs=num_faqs,
target_audience=TargetAudience(target_audience),
faq_style=FAQStyle(faq_style),
include_emojis=include_emojis,
include_code_examples=include_code_examples,
include_references=include_references,
search_depth=SearchDepth(search_depth),
time_range=time_range,
language=language
)
# Initialize generator
generator = FAQGenerator(config)
# Generate FAQs
with st.spinner("Generating FAQs..."):
faqs = asyncio.run(generator.generate_faqs(content))
# Display results
st.success("FAQs generated successfully!")
# Step 1: Generate search queries
if content and not st.session_state.search_queries:
if st.button("Generate Search Queries"):
with st.spinner("Generating search queries..."):
search_queries = st.session_state.generator.generate_search_queries(content)
if search_queries:
st.session_state.search_queries = search_queries
st.session_state.selected_queries = [] # Reset selected queries
st.session_state.research_completed = False # Reset research status
st.session_state.research_results = {} # Reset research results
st.session_state.faq_config = None # Reset config
st.session_state.generated_faqs = None # Reset generated FAQs
st.success("Search queries generated successfully!")
# Step 2: Display and select search queries
if st.session_state.search_queries:
st.subheader("Select Search Queries")
st.info("Select the queries you want to use for web research. You can select multiple queries.")
# Create checkboxes for each search query
selected_queries = []
for query in st.session_state.search_queries:
if st.checkbox(query, key=f"query_{query}", value=query in st.session_state.selected_queries):
selected_queries.append(query)
# Update selected queries in session state
st.session_state.selected_queries = selected_queries
# Step 3: Do web research
if st.session_state.selected_queries and not st.session_state.research_completed:
if st.button("Do Web Research"):
try:
# Create config with selected queries
config = FAQConfig(
num_faqs=num_faqs,
target_audience=TargetAudience(target_audience),
faq_style=FAQStyle(faq_style),
include_emojis=include_emojis,
include_code_examples=include_code_examples,
include_references=include_references,
search_depth=SearchDepth(search_depth),
time_range=time_range,
language=language,
selected_search_queries=selected_queries
)
# Store config in session state
st.session_state.faq_config = config
# Update generator with config
st.session_state.generator.config = config
# Do research
with st.spinner("Conducting web research..."):
research_results = st.session_state.generator._conduct_research(content)
st.session_state.research_completed = True
st.session_state.research_results = research_results
st.success("Web research completed successfully!")
# Display research results
st.subheader("Research Results")
for query, results in research_results.items():
with st.expander(f"Results for: {query}"):
if isinstance(results, dict):
st.json(results)
else:
st.text(results)
except Exception as e:
st.error(f"Error during web research: {str(e)}")
st.error("Please try again with different search queries or adjust the search depth.")
# Step 4: Generate FAQs
if st.session_state.research_completed and st.session_state.research_results and st.session_state.faq_config:
if st.button("Generate FAQs"):
try:
# Update generator with stored config
st.session_state.generator.config = st.session_state.faq_config
# Generate FAQs
with st.spinner("Generating FAQs..."):
logger.info("Starting FAQ generation...")
faqs = st.session_state.generator.generate_faqs(content)
logger.info(f"Generated {len(faqs) if faqs else 0} FAQs")
if not faqs:
st.error("No FAQs were generated. Please try again.")
return
st.session_state.generated_faqs = faqs
st.success("FAQs generated successfully!")
except Exception as e:
logger.error(f"Error generating FAQs: {str(e)}")
st.error(f"Error generating FAQs: {str(e)}")
st.error("Please try again or adjust your settings.")
# Display generated FAQs if they exist
if st.session_state.generated_faqs:
st.subheader("Generated FAQs")
# Output format selection
output_format = st.radio(
"Output Format",
["Preview", "Markdown", "HTML", "JSON"]
["Preview", "Markdown", "HTML", "JSON"],
key="output_format"
)
# Create columns for copy and download buttons
col1, col2 = st.columns(2)
if output_format == "Preview":
for i, faq in enumerate(faqs, 1):
# Create a formatted text for copying
preview_text = ""
for i, faq in enumerate(st.session_state.generated_faqs, 1):
preview_text += f"{i}. {faq.question}\n"
preview_text += f"{faq.answer}\n\n"
if faq.code_example:
preview_text += f"Code Example:\n{faq.code_example}\n\n"
if faq.references:
preview_text += "References:\n"
for ref in faq.references:
preview_text += f"- {ref['source']}\n"
preview_text += "\n"
with col1:
if st.button("Copy to Clipboard", key="copy_preview"):
copy_to_clipboard(preview_text)
# Display the FAQs
for i, faq in enumerate(st.session_state.generated_faqs, 1):
with st.expander(f"{i}. {faq.question}"):
st.markdown(faq.answer)
if faq.code_example:
@@ -140,38 +261,52 @@ def main():
if faq.references:
st.markdown("**References:**")
for ref in faq.references:
st.markdown(f"- [{ref['title']}]({ref['url']}) - {ref['source']} ({ref['date']})")
st.markdown(f"- {ref['source']}")
elif output_format == "Markdown":
st.code(generator.to_markdown(), language="markdown")
st.download_button(
"Download Markdown",
generator.to_markdown(),
file_name="faqs.md",
mime="text/markdown"
)
markdown_output = st.session_state.generator.to_markdown()
st.code(markdown_output, language="markdown")
with col1:
if st.button("Copy to Clipboard", key="copy_markdown"):
copy_to_clipboard(markdown_output)
with col2:
st.download_button(
"Download Markdown",
markdown_output,
file_name="faqs.md",
mime="text/markdown"
)
elif output_format == "HTML":
st.code(generator.to_html(), language="html")
st.download_button(
"Download HTML",
generator.to_html(),
file_name="faqs.html",
mime="text/html"
)
html_output = st.session_state.generator.to_html()
st.code(html_output, language="html")
with col1:
if st.button("Copy to Clipboard", key="copy_html"):
copy_to_clipboard(html_output)
with col2:
st.download_button(
"Download HTML",
html_output,
file_name="faqs.html",
mime="text/html"
)
elif output_format == "JSON":
json_output = json.dumps([faq.__dict__ for faq in faqs], indent=2)
json_output = json.dumps([faq.__dict__ for faq in st.session_state.generated_faqs], indent=2)
st.code(json_output, language="json")
st.download_button(
"Download JSON",
json_output,
file_name="faqs.json",
mime="application/json"
)
except Exception as e:
st.error(f"Error generating FAQs: {str(e)}")
with col1:
if st.button("Copy to Clipboard", key="copy_json"):
copy_to_clipboard(json_output)
with col2:
st.download_button(
"Download JSON",
json_output,
file_name="faqs.json",
mime="application/json"
)
if __name__ == "__main__":
main()

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

@@ -0,0 +1,758 @@
"""
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

@@ -0,0 +1,236 @@
"""
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

@@ -0,0 +1,493 @@
"""
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

@@ -0,0 +1,545 @@
"""
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

@@ -0,0 +1,988 @@
"""
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

@@ -0,0 +1,557 @@
# 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

@@ -0,0 +1,336 @@
"""
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
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 = {}
async def generate_outline(self, topic: str) -> Dict:
"""Generate a comprehensive outline based on the topic and configuration."""
try:
# Step 1: Generate main sections
main_sections = await self._generate_main_sections(topic)
# Step 2: Generate subsections for each main section
detailed_sections = await self._generate_subsections(main_sections)
# Step 3: Add introduction and conclusion if requested
if self.config.include_introduction:
detailed_sections["Introduction"] = await self._generate_introduction(topic)
if self.config.include_conclusion:
detailed_sections["Conclusion"] = await self._generate_conclusion(topic)
# Step 4: Add FAQs if requested
if self.config.include_faqs:
detailed_sections["FAQs"] = await self._generate_faqs(topic)
# Step 5: Add resources if requested
if self.config.include_resources:
detailed_sections["Additional Resources"] = await self._generate_resources(topic)
self.outline = detailed_sections
# Step 6: Generate content for each section
await self._generate_section_contents(topic)
return self.outline
except Exception as err:
logger.error(f"Failed to generate outline: {err}")
raise
async def _generate_main_sections(self, topic: str) -> List[str]:
"""Generate main sections for the outline."""
prompt = f"""Generate {self.config.num_main_sections} main sections for a {self.config.content_type.value}
article about {topic} with the following characteristics:
Content Type: {self.config.content_type.value}
Content Depth: {self.config.content_depth.value}
Target Word Count: {self.config.target_word_count}
Target Audience: {self.config.target_audience}
Style: {self.config.outline_style.value}
Additional Requirements:
- Each section should contribute to the overall word count goal
- Sections should flow logically
- Include key concepts and important points
- Consider SEO optimization
- Keywords to include: {', '.join(self.config.keywords or [])}
- Topics to exclude: {', '.join(self.config.exclude_topics or [])}
Please provide only the section titles, one per line."""
response = await llm_text_gen(prompt)
return [section.strip() for section in response.split('\n') if section.strip()]
async def _generate_subsections(self, main_sections: List[str]) -> Dict[str, List[str]]:
"""Generate subsections for each main section."""
detailed_sections = {}
for section in main_sections:
prompt = f"""Generate {self.config.num_subsections_per_section} subsections for the following section:
{section}
Content Type: {self.config.content_type.value}
Content Depth: {self.config.content_depth.value}
Style: {self.config.outline_style.value}
Each subsection should:
- Be specific and focused
- Support the main section's topic
- Include key points to cover
- Consider SEO optimization
Please provide only the subsection titles, one per line."""
response = await llm_text_gen(prompt)
detailed_sections[section] = [sub.strip() for sub in response.split('\n') if sub.strip()]
return detailed_sections
async def _generate_introduction(self, topic: str) -> List[str]:
"""Generate introduction subsections."""
prompt = f"""Generate introduction subsections for an article about {topic}.
Content Type: {self.config.content_type.value}
Content Depth: {self.config.content_depth.value}
Style: {self.config.outline_style.value}
The introduction should:
- Hook the reader
- Present the main topic
- Outline what's to come
- Set the tone for the article
Please provide only the subsection titles, one per line."""
response = await llm_text_gen(prompt)
return [sub.strip() for sub in response.split('\n') if sub.strip()]
async def _generate_conclusion(self, topic: str) -> List[str]:
"""Generate conclusion subsections."""
prompt = f"""Generate conclusion subsections for an article about {topic}.
Content Type: {self.config.content_type.value}
Content Depth: {self.config.content_depth.value}
Style: {self.config.outline_style.value}
The conclusion should:
- Summarize key points
- Provide final thoughts
- Include a call to action
- Leave a lasting impression
Please provide only the subsection titles, one per line."""
response = await llm_text_gen(prompt)
return [sub.strip() for sub in response.split('\n') if sub.strip()]
async def _generate_faqs(self, topic: str) -> List[str]:
"""Generate FAQ subsections."""
prompt = f"""Generate FAQ subsections for an article about {topic}.
Content Type: {self.config.content_type.value}
Content Depth: {self.config.content_depth.value}
Style: {self.config.outline_style.value}
The FAQs should:
- Address common questions
- Cover important aspects
- Be relevant to the target audience
- Include both basic and advanced questions
Please provide only the FAQ questions, one per line."""
response = await llm_text_gen(prompt)
return [sub.strip() for sub in response.split('\n') if sub.strip()]
async def _generate_resources(self, topic: str) -> List[str]:
"""Generate resource subsections."""
prompt = f"""Generate resource subsections for an article about {topic}.
Content Type: {self.config.content_type.value}
Content Depth: {self.config.content_depth.value}
Style: {self.config.outline_style.value}
The resources should:
- Include relevant links
- Suggest further reading
- Provide tools or references
- Include related materials
Please provide only the resource categories, one per line."""
response = await llm_text_gen(prompt)
return [sub.strip() for sub in response.split('\n') if sub.strip()]
async def _generate_section_contents(self, topic: str):
"""Generate content and images for each section."""
for section, subsections in self.outline.items():
if section not in ["Introduction", "Conclusion", "FAQs", "Additional Resources"]:
# Generate content for the main section
content_prompt = f"""Write a detailed section for a blog post about {topic}.
Section Title: {section}
Content Type: {self.config.content_type.value}
Content Depth: {self.config.content_depth.value}
Style: {self.config.outline_style.value}
Target Word Count: {self.config.target_word_count // self.config.num_main_sections}
Include:
- Clear explanation of the main points
- Examples and illustrations
- Key takeaways
- Relevant data or statistics
"""
content = await llm_text_gen(content_prompt)
# Generate image prompt if images are enabled
image_prompt = None
image_path = None
if self.config.include_images:
image_prompt = f"""Create a detailed image prompt for a blog section about {topic}.
Section: {section}
Content: {content[:200]}...
Style: {self.config.image_style}
"""
image_prompt = await llm_text_gen(image_prompt)
try:
image_path = generate_image(
image_prompt,
title=section,
description=content[:100],
tags=self.config.keywords
)
except Exception as err:
logger.warning(f"Failed to generate image for section {section}: {err}")
self.section_contents[section] = SectionContent(
title=section,
content=content,
image_prompt=image_prompt,
image_path=image_path
)
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

@@ -0,0 +1,489 @@
"""
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;
}
.stButton>button:hover {
background-color: #45a049;
}
.section-card {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.content-preview {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin: 10px 0;
}
.image-container {
display: flex;
justify-content: center;
margin: 20px 0;
}
.stats-card {
background-color: #e8f5e9;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
}
.edit-section {
background-color: #e3f2fd;
padding: 15px;
border-radius: 4px;
margin: 10px 0;
}
.subsection-list {
margin-left: 20px;
}
</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">
<h2>{section}</h2>
""", 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)
# Display 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:
# Edit content
edited_content = edit_section_content(section, content.content)
content.content = edited_content
# Edit subsections
edited_subsections = edit_subsections(section, subsections)
subsections[:] = edited_subsections
# Edit metadata
if generator:
edit_section_metadata(section, generator)
# Display subsections
st.markdown("### Subsections")
st.markdown('<div class="subsection-list">', unsafe_allow_html=True)
for subsection in subsections:
st.markdown(f"- {subsection}")
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():
st.set_page_config(
page_title="Blog Outline Generator",
page_icon="📝",
layout="wide",
initial_sidebar_state="expanded"
)
# 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.
""")
# Sidebar for configuration
with st.sidebar:
st.header("Configuration")
# Basic settings
topic = st.text_input("Blog Topic", placeholder="Enter your blog topic")
content_type = st.selectbox(
"Content Type",
[type.value for type in ContentType]
)
content_depth = st.selectbox(
"Content Depth",
[depth.value for depth in ContentDepth]
)
outline_style = st.selectbox(
"Outline Style",
[style.value for style in OutlineStyle]
)
# Content structure
st.subheader("Content Structure")
target_word_count = st.slider("Target Word Count", 500, 5000, 2000, 100)
num_main_sections = st.slider("Number of Main Sections", 3, 10, 5)
num_subsections = st.slider("Subsections per Section", 2, 5, 3)
# Advanced settings
with st.expander("Advanced Settings"):
include_intro = st.checkbox("Include Introduction", value=True)
include_conclusion = st.checkbox("Include Conclusion", value=True)
include_faqs = st.checkbox("Include FAQs", value=True)
include_resources = st.checkbox("Include Resources", value=True)
# Image settings
st.subheader("Image Settings")
include_images = st.checkbox("Include Images", value=True)
if include_images:
image_style = st.selectbox(
"Image Style",
["realistic", "illustration", "minimalist", "photographic", "artistic"]
)
image_engine = st.selectbox(
"Image Engine",
["Gemini-AI", "Dalle3", "Stability-AI"]
)
# Target audience and language
st.subheader("Target Audience")
target_audience = st.text_input("Target Audience", value="general")
language = st.text_input("Language", value="English")
# Keywords and exclusions
st.subheader("Content Optimization")
keywords = st.text_area("Keywords (comma-separated)")
exclude_topics = st.text_area("Topics to Exclude (comma-separated)")
# Main content area
if topic:
# 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)
# Generate outline
if st.button("Generate Outline"):
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 = asyncio.run(generator.generate_outline(topic))
# Display results
st.success("Outline generated successfully!")
# 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
for section, subsections in outline.items():
content = generator.section_contents.get(section)
display_section(section, subsections, content)
elif output_format == "Markdown":
st.code(generator.to_markdown(), language="markdown")
st.download_button(
"Download Markdown",
generator.to_markdown(),
file_name="blog_outline.md",
mime="text/markdown"
)
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.code(json_output, language="json")
st.download_button(
"Download JSON",
json_output,
file_name="blog_outline.json",
mime="application/json"
)
elif output_format == "HTML":
# Add HTML export functionality
html_output = f"""
<!DOCTYPE html>
<html>
<head>
<title>{topic} - Blog Outline</title>
<style>
body {{ font-family: Arial, sans-serif; max-width: 800px; 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.code(html_output, language="html")
st.download_button(
"Download HTML",
html_output,
file_name="blog_outline.html",
mime="text/html"
)
except Exception as e:
st.error(f"Error generating outline: {str(e)}")
else:
st.info("Please enter a blog topic to get started.")
if __name__ == "__main__":
main()

View File

@@ -8,6 +8,7 @@ from lib.ai_writers.linkedin_writer import LinkedInAIWriter
from lib.ai_writers.blog_rewriter_updater.ai_blog_rewriter import write_blog_rewriter
from lib.ai_writers.ai_blog_faqs_writer.faqs_ui import main as faqs_generator
from lib.ai_writers.ai_blog_writer.ai_blog_generator import ai_blog_writer_page
from lib.ai_writers.ai_outline_writer.outline_ui import main as outline_generator
from loguru import logger
def list_ai_writers():
@@ -92,6 +93,14 @@ def list_ai_writers():
"category": "Content Creation",
"function": faqs_generator,
"path": "faqs_generator"
},
{
"name": "Blog Outline Generator",
"icon": "📋",
"description": "Create detailed blog outlines with AI-powered content generation and image integration",
"category": "Content Creation",
"function": outline_generator,
"path": "outline_generator"
}
]

View File

@@ -1,15 +0,0 @@
def get_blog_conclusion(blog_content):
"""
Accepts a blog content and concludes it.
"""
prompt = f"""As an expert SEO and blog writer, please conclude the given blog providing vital take aways,
summarise key points (no more than 300 characters) in bullet points. The blog content: {blog_content}
"""
logger.info(f"Generating blog conclusion iwth prompt: {prompt}")
try:
# TBD: Add logic for which_provider and which_model
response = openai_chatgpt(prompt)
except Exception as err:
SystemError(f"Error in generating blog conclusion: {err}")
else:
return response

View File

@@ -1,16 +0,0 @@
def get_blog_intro(blog_title, blog_topics):
"""
Generate blog introduction as per title and sub topics
"""
prompt = f"""As a skilled wordsmith, I'll equip you with a blog title and relevant topics, tasking you with crafting an engaging introduction. Your challenge: Create a brief, compelling entry that entices readers to explore the entire post. This introduction must be concise (under 250 characters) yet powerful, clearly stating the blog's purpose and what readers stand to gain. Reply with only the introduction.
Intrigue your audience from the start with vibrant language, employing strong verbs and vivid descriptions. Address a common challenge your readers face, demonstrating empathy and positioning yourself as their go-to expert. Pose thought-provoking questions that prompt reader engagement and contemplation.
Remember, your words matter. This introduction serves as the cornerstone of the blog post. It should not only captivate attention but also encourage deeper exploration. Additionally, strategically integrate relevant keywords to enhance visibility on search engine results pages (SERPs). Your mission: Craft a blog introduction that resonates, leaving readers eager to delve further into the titled piece: '{blog_title}', covering these sub-topics: {blog_topics}."""
try:
# TBD: Add logic for which_provider and which_model
response = openai_chatgpt(prompt)
except Exception as err:
SystemError(f"Error in generating Blog Introduction: {err}")
return response

View File

@@ -1,18 +0,0 @@
def generate_topic_outline(blog_title, num_subtopics):
"""
Given a blog title generate an outline for it
"""
# TBD: Remove hardcoding, make dynamic
prompt = f"""As a SEO expert, suggest only {num_subtopics} beginner-friendly and
insightful sub topics for the blog title: {blog_title}.
Respond with only answer and no description, explanations."""
# The suggested {num_subtopics} outline should include few long-tailed keywords and most popular questions.
# TBD: Include --niche
logger.info(f"Prompt used for blog title Outline :\n{prompt}\n")
# TBD: Add logic for which_provider and which_model
try:
response = openai_chatgpt(prompt)
except Exception as err:
SystemError(f"Error in generating Blog Title: {err}")
return response

View File

@@ -1,47 +0,0 @@
def generate_blog_topics(blog_keywords, num_blogs, niche):
"""
For a given prompt, generate blog topics.
Using the davinci-instruct-beta-v3 model. Its proven to be an ideal
one for generating unique blog content.
Ex: Generate SEO optimized blog topics on given keywords
"""
prompt = f"""As an SEO specialist and blog writer, write {num_blogs} catchy
and SEO-friendly blog topics on {blog_keywords}. The blog title must be less than 80 characters.
The blog titles must follow best SEO practises, be engaging and invite/tempt users to read full blog.
Do not include descriptions, explanations. Do not number the result."""
# Beware of keywords stuffing, clustering, semantic should help avoid.
if num_blogs > 5:
# Get more keywords, based on user given keywords.
more_keywords = get_related_keywords(num_blogs, blog_keywords, niche)
prompt = prompt + """Use the following keywords wisely, without keyword stuffing: {more_keywords}"""
logger.info(f"Prompt used for generating blog topics: \n{prompt}\n")
try:
response = openai_chatgpt(prompt)
return response
except Exception as err:
SystemError(f"Error in generating blog topics: {err}")
def get_related_keywords(num_blogs, keywords, niche):
"""
Helper function to get more keywords from GPTs.
"""
# Check if niche: use long tailed, else use popular keywords.
if niche:
prompt = (f"Generate a list without description of the top {num_blogs} most popular and semantically"
f"related long-tailed keywords and entities for the topic of {keywords} that are used in"
"high-quality content and relevant to my competitors."
)
else:
prompt = (f"Generate a list without description of the top {num_blogs} most popular and"
f" semantically related keywords and entities for the topic of {keywords} that are used"
" in high-quality content and relevant to my competitors."
)
try:
# TBD: Add logic for which_provider and which_model
response = openai_chatgpt(prompt)
return response
except Exception as err:
SystemError(f"Error in getting related keywords.")

View File

@@ -1,37 +0,0 @@
"""
At the command line, only need to run once to install the package via pip:
$ pip install google-generativeai
"""
from .gpt_providers.gemini_pro_text import gemini_text_response
def gemini_get_code_samples(blog_article):
""" Provide a programming blog and get code exmaples."""
prompt = f"""As an expert programmer and copywriter, I will provide you with blog article.
Your task is to research and write one code example for the given blog article.
Do not include your explanations in response.
Blog Article: '{blog_article}' """
try:
code_sample = gemini_text_response(prompt)
response = combine_blog_code_sample(blog_article, code_sample)
return response
except Exception as err:
raise ValueError(f"Failed to get response from Gemini pro: {err}")
def combine_blog_code_sample(blog_article, code_sample):
""" Include the code sample into the given blog. """
prompt = """You are expert document editor, I will provide you blog article and a code sample.
Your task is to edit the given blog article to include the code sample after the introduction section.
Do not modify the content of the given blog article. Your response should include the whole blog_article with
the code sample added to it.
Adopt the formatting of the given blog article. Do not include explanations of your response.
Edit the given blog to include the code sample in it.
Blog Article: {blog_article}\n
Code sample: {code_sample}\n"""
try:
response = gemini_text_response(prompt)
return response
except Exception as err:
raise ValueError(f"Failed to combine blog and code: {err}")

View File

@@ -1,19 +0,0 @@
def generate_topic_content(blog_keywords, sub_topic):
"""
For each of given topic generate content for it.
"""
# The outline should contain various subheadings and include the starting sentence for each section.
# TBD: Depending on the usecase 'Voice and style' will change to professional etc.
prompt = f"""As a professional blogger and topic authority on {blog_keywords},
craft factual (no more than 200 characters) subtopic content on {sub_topic}.
Your response should reflect Experience, Expertise, Authoritativeness and Trustworthiness from content.
Voice and style guide: Write in a professional manner, giving enlightening details and reasons.
Use natural language and phrases that a real person would use: in normal conversations.
Format your response using markdown. REMEMBER Not to include introduction or conclusion in your response.
Use headings(h3 to h6 only), subheadings, bullet points, and bold to organize the information."""
logger.info(f"Generate topic content using prompt:\n{prompt}\n")
try:
response = openai_chatgpt(prompt)
return response
except Exception as err:
SystemError(f"Error in generating topic content: {err}")

View File

@@ -0,0 +1,208 @@
# Wix Blog Integration for Alwrity
This integration allows you to publish blog content from Alwrity directly to your Wix site using the Wix REST API.
## Features
- **Blog Post Management**: Create, update, and delete blog posts
- **Media Management**: Upload images and other media files
- **SEO Optimization**: Comprehensive SEO settings and analysis
- **Category Management**: Create and manage blog categories
- **Markdown Support**: Write in markdown and publish as HTML
- **Streamlit UI**: User-friendly interface for publishing
## Prerequisites
Before using this integration, you'll need:
1. A Wix site with the Blog feature enabled
2. Wix API credentials (refresh token and site ID)
3. Python 3.7+ with required dependencies
## Getting Wix API Credentials
To use this integration, you need to obtain a refresh token and site ID from Wix:
1. **Create a Wix Developer Account**:
- Go to [Wix Developers](https://dev.wix.com/) and sign up or log in
- Create a new OAuth app
2. **Configure OAuth App**:
- Set a name and description for your app
- Add redirect URLs (e.g., `https://localhost:3000/oauth/callback`)
- Save the app and note the App ID and App Secret
3. **Get a Refresh Token**:
- Follow the OAuth flow to get an authorization code
- Exchange the code for an access token and refresh token
- Detailed instructions: [Wix OAuth Documentation](https://dev.wix.com/api/rest/getting-started/authentication)
4. **Get Your Site ID**:
- Log in to your Wix account
- Go to your site's dashboard
- The site ID is in the URL: `https://manage.wix.com/dashboard/{SITE_ID}/home`
## Installation
The Wix integration is included with Alwrity. No additional installation is required.
## Usage
### Using the Streamlit UI
1. Navigate to the Wix integration in the Alwrity UI
2. Enter your Wix refresh token and site ID
3. Fill in the blog details and content
4. Click "Publish to Wix"
### Using the Python API
```python
from lib.integrations.wix_integration import WixIntegration
# Initialize the integration
wix = WixIntegration(
refresh_token="YOUR_REFRESH_TOKEN",
site_id="YOUR_SITE_ID"
)
# Publish a blog post
result = wix.publish_blog_post(
title="My Blog Post",
content="# Hello World\n\nThis is my blog post.",
is_markdown=True,
tags=["example", "blog"],
categories=["Technology"],
publish=True
)
# Get the published post URL
post_url = result.get("post", {}).get("url")
print(f"Published at: {post_url}")
```
### Using the Command-Line Interface
```bash
# Set environment variables
export WIX_REFRESH_TOKEN="YOUR_REFRESH_TOKEN"
export WIX_SITE_ID="YOUR_SITE_ID"
# List blog posts
python -m lib.integrations.wix_cli list-posts
# Publish a blog post
python -m lib.integrations.wix_cli publish-post \
--title "My Blog Post" \
--content-file blog.md \
--is-markdown \
--tags "example,blog" \
--categories "Technology"
# Generate an SEO report
python -m lib.integrations.wix_cli seo-report \
--title "My Blog Post" \
--keywords "example,blog,technology"
```
## API Reference
### WixIntegration
The main integration class that provides high-level methods for working with Wix blogs.
#### Methods
- `publish_blog_post(title, content, ...)`: Publish a blog post
- `upload_media(file_path, ...)`: Upload a media file
- `get_seo_report(post_id, target_keywords)`: Generate an SEO report
- `list_blog_posts(limit, offset, ...)`: List blog posts
- `list_categories()`: List blog categories
- `create_category(name, description)`: Create a blog category
- `get_post_by_id(post_id)`: Get a blog post by ID
- `get_post_by_title(title)`: Get a blog post by title
- `delete_post(post_id)`: Delete a blog post
### WixAPIClient
Low-level client for interacting with the Wix API.
### WixBlogManager
Handles blog content management, including markdown processing and image handling.
### WixSEOOptimizer
Provides SEO analysis and optimization for blog posts.
## Error Handling
The integration includes comprehensive error handling:
- API errors are logged with detailed information
- Authentication errors provide clear guidance
- File handling errors include path information
- Network errors include retry logic
## Best Practices
1. **Store credentials securely**:
- Use environment variables or a secure credential store
- Don't hardcode credentials in your code
2. **Optimize images before upload**:
- Compress images to reduce file size
- Use appropriate image formats (JPEG for photos, PNG for graphics)
3. **SEO optimization**:
- Use the SEO report to improve your content
- Include relevant keywords in titles and headings
- Add alt text to all images
4. **Content management**:
- Use categories and tags consistently
- Include featured images for better visual appeal
- Write clear, concise meta descriptions
## Troubleshooting
### Common Issues
1. **Authentication Errors**:
- Ensure your refresh token is valid
- Check that your site ID is correct
- Verify that your app has the necessary permissions
2. **API Rate Limits**:
- The Wix API has rate limits that may affect bulk operations
- Add delays between requests if you're publishing many posts
3. **Image Upload Issues**:
- Check that the image file exists and is readable
- Verify that the image format is supported (JPEG, PNG, GIF)
- Ensure the image file size is within Wix limits
4. **Content Formatting Issues**:
- If using markdown, ensure it's valid
- Check for special characters that might cause issues
- Verify that HTML content is properly formatted
### Getting Help
If you encounter issues not covered here:
1. Check the logs for detailed error messages
2. Consult the [Wix API Documentation](https://dev.wix.com/api/rest/getting-started)
3. Contact Alwrity support for assistance
## License
This integration is part of the Alwrity platform and is subject to the same license terms.
## Acknowledgements
- [Wix REST API](https://dev.wix.com/api/rest) for providing the API endpoints
- [Requests](https://docs.python-requests.org/) for HTTP functionality
- [Markdown](https://python-markdown.github.io/) for markdown processing
- [BeautifulSoup](https://www.crummy.com/software/BeautifulSoup/) for HTML parsing
- [Streamlit](https://streamlit.io/) for the user interface

View File

@@ -0,0 +1,850 @@
"""
Wix API Client for Blog Management
This module provides a comprehensive client for interacting with the Wix API
to manage blog posts, SEO settings, and media uploads.
Documentation: https://dev.wix.com/api/rest/getting-started
"""
import os
import json
import time
import logging
import requests
from typing import Dict, List, Optional, Union, Any, Tuple
from datetime import datetime
import mimetypes
from pathlib import Path
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('wix_api_client')
class WixAPIClient:
"""
Client for interacting with the Wix API for blog management.
This client handles authentication, blog post creation/updating,
media uploads, and SEO settings.
"""
# Base URLs for different Wix API endpoints
BASE_URL = "https://www.wixapis.com"
OAUTH_URL = "https://www.wix.com/oauth"
# API Endpoints
BLOG_API = "/blog/v3"
MEDIA_API = "/site-media/v1"
SEO_API = "/site-properties/v4/seo"
def __init__(
self,
api_key: Optional[str] = None,
refresh_token: Optional[str] = None,
site_id: Optional[str] = None
):
"""
Initialize the Wix API Client.
Args:
api_key: Wix API key (optional if using refresh token)
refresh_token: Wix refresh token for OAuth authentication
site_id: Wix site ID
"""
self.api_key = api_key or os.environ.get('WIX_API_KEY')
self.refresh_token = refresh_token or os.environ.get('WIX_REFRESH_TOKEN')
self.site_id = site_id or os.environ.get('WIX_SITE_ID')
self.access_token = None
self.token_expiry = 0
if not self.refresh_token:
logger.warning("No refresh token provided. Authentication will fail.")
if not self.site_id:
logger.warning("No site ID provided. API calls will fail.")
def _get_headers(self) -> Dict[str, str]:
"""
Get the headers required for API requests.
Returns:
Dict containing the necessary headers for Wix API requests
"""
# Ensure we have a valid access token
self._ensure_valid_token()
headers = {
"Authorization": f"Bearer {self.access_token}",
"wix-site-id": self.site_id,
"Content-Type": "application/json"
}
return headers
def _ensure_valid_token(self) -> None:
"""
Ensure we have a valid access token, refreshing if necessary.
"""
current_time = time.time()
# If token is expired or doesn't exist, refresh it
if not self.access_token or current_time >= self.token_expiry:
self._refresh_access_token()
def _refresh_access_token(self) -> None:
"""
Refresh the access token using the refresh token.
"""
if not self.refresh_token:
raise ValueError("Refresh token is required for authentication")
url = f"{self.OAUTH_URL}/access"
payload = {
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
"client_id": self.api_key if self.api_key else ""
}
try:
response = requests.post(url, json=payload)
response.raise_for_status()
data = response.json()
self.access_token = data.get("access_token")
# Set token expiry (subtract 5 minutes for safety margin)
expires_in = data.get("expires_in", 3600) # Default to 1 hour if not specified
self.token_expiry = time.time() + expires_in - 300
logger.info("Successfully refreshed access token")
except requests.exceptions.RequestException as e:
logger.error(f"Failed to refresh access token: {str(e)}")
if response.text:
logger.error(f"Response: {response.text}")
raise
def _make_request(
self,
method: str,
endpoint: str,
data: Optional[Dict] = None,
params: Optional[Dict] = None,
files: Optional[Dict] = None
) -> Dict:
"""
Make a request to the Wix API.
Args:
method: HTTP method (GET, POST, PUT, DELETE)
endpoint: API endpoint
data: Request payload
params: Query parameters
files: Files to upload
Returns:
Response data as dictionary
"""
url = f"{self.BASE_URL}{endpoint}"
headers = self._get_headers()
# If we're uploading files, remove the Content-Type header
if files:
headers.pop("Content-Type", None)
try:
response = requests.request(
method=method,
url=url,
headers=headers,
json=data,
params=params,
files=files
)
# Log request details for debugging
logger.debug(f"Request: {method} {url}")
logger.debug(f"Headers: {headers}")
if data:
logger.debug(f"Data: {json.dumps(data)}")
if params:
logger.debug(f"Params: {params}")
# Handle response
response.raise_for_status()
if response.content:
return response.json()
return {}
except requests.exceptions.HTTPError as e:
logger.error(f"HTTP error: {str(e)}")
if response.text:
logger.error(f"Response: {response.text}")
raise
except requests.exceptions.RequestException as e:
logger.error(f"Request error: {str(e)}")
raise
# Blog Post Management
def list_posts(
self,
limit: int = 50,
offset: int = 0,
sort_field: str = "lastPublishedDate",
sort_order: str = "desc",
filter_by: Optional[Dict] = None
) -> Dict:
"""
List blog posts with pagination and sorting.
Args:
limit: Maximum number of posts to return (default: 50)
offset: Pagination offset (default: 0)
sort_field: Field to sort by (default: lastPublishedDate)
sort_order: Sort order, 'asc' or 'desc' (default: desc)
filter_by: Optional filter criteria
Returns:
Dictionary containing blog posts and pagination info
"""
endpoint = f"{self.BLOG_API}/posts/query"
payload = {
"limit": limit,
"offset": offset,
"sort": [
{
"fieldName": sort_field,
"order": sort_order
}
]
}
if filter_by:
payload["filter"] = filter_by
return self._make_request("POST", endpoint, data=payload)
def get_post(self, post_id: str) -> Dict:
"""
Get a specific blog post by ID.
Args:
post_id: ID of the blog post
Returns:
Blog post data
"""
endpoint = f"{self.BLOG_API}/posts/{post_id}"
return self._make_request("GET", endpoint)
def create_post(
self,
title: str,
content: str,
excerpt: Optional[str] = None,
featured_image_id: Optional[str] = None,
tags: Optional[List[str]] = None,
categories: Optional[List[str]] = None,
seo_data: Optional[Dict] = None,
publish: bool = False
) -> Dict:
"""
Create a new blog post.
Args:
title: Post title
content: Post content (HTML)
excerpt: Post excerpt/summary
featured_image_id: ID of the featured image (from media manager)
tags: List of tags
categories: List of category IDs
seo_data: SEO settings for the post
publish: Whether to publish the post immediately
Returns:
Created blog post data
"""
endpoint = f"{self.BLOG_API}/posts"
# Prepare the post data
post_data = {
"post": {
"title": title,
"content": content,
"excerpt": excerpt or "",
"featured_image_id": featured_image_id,
"tags": tags or [],
"categoryIds": categories or []
}
}
# Add SEO data if provided
if seo_data:
post_data["post"]["seoData"] = seo_data
# Create the post
response = self._make_request("POST", endpoint, data=post_data)
# Publish the post if requested
if publish and response.get("post", {}).get("id"):
post_id = response["post"]["id"]
self.publish_post(post_id)
# Refresh the post data to get the published version
response = self.get_post(post_id)
return response
def update_post(
self,
post_id: str,
title: Optional[str] = None,
content: Optional[str] = None,
excerpt: Optional[str] = None,
featured_image_id: Optional[str] = None,
tags: Optional[List[str]] = None,
categories: Optional[List[str]] = None,
seo_data: Optional[Dict] = None,
publish: bool = False
) -> Dict:
"""
Update an existing blog post.
Args:
post_id: ID of the post to update
title: New post title (optional)
content: New post content (HTML) (optional)
excerpt: New post excerpt/summary (optional)
featured_image_id: New featured image ID (optional)
tags: New list of tags (optional)
categories: New list of category IDs (optional)
seo_data: New SEO settings (optional)
publish: Whether to publish the post after updating
Returns:
Updated blog post data
"""
# First, get the current post data
current_post = self.get_post(post_id)
if "post" not in current_post:
raise ValueError(f"Post with ID {post_id} not found")
current_post_data = current_post["post"]
# Update only the fields that were provided
update_data = {
"post": {
"id": post_id,
"title": title if title is not None else current_post_data.get("title", ""),
"content": content if content is not None else current_post_data.get("content", ""),
"excerpt": excerpt if excerpt is not None else current_post_data.get("excerpt", ""),
"featured_image_id": featured_image_id if featured_image_id is not None else current_post_data.get("featuredImageId"),
"tags": tags if tags is not None else current_post_data.get("tags", []),
"categoryIds": categories if categories is not None else current_post_data.get("categoryIds", [])
}
}
# Add SEO data if provided
if seo_data:
update_data["post"]["seoData"] = seo_data
elif "seoData" in current_post_data:
update_data["post"]["seoData"] = current_post_data["seoData"]
# Update the post
endpoint = f"{self.BLOG_API}/posts/{post_id}"
response = self._make_request("PATCH", endpoint, data=update_data)
# Publish the post if requested
if publish:
self.publish_post(post_id)
# Refresh the post data to get the published version
response = self.get_post(post_id)
return response
def delete_post(self, post_id: str) -> Dict:
"""
Delete a blog post.
Args:
post_id: ID of the post to delete
Returns:
Response data
"""
endpoint = f"{self.BLOG_API}/posts/{post_id}"
return self._make_request("DELETE", endpoint)
def publish_post(self, post_id: str) -> Dict:
"""
Publish a draft blog post.
Args:
post_id: ID of the post to publish
Returns:
Published post data
"""
endpoint = f"{self.BLOG_API}/posts/{post_id}/publish"
return self._make_request("POST", endpoint)
def unpublish_post(self, post_id: str) -> Dict:
"""
Unpublish a published blog post (revert to draft).
Args:
post_id: ID of the post to unpublish
Returns:
Unpublished post data
"""
endpoint = f"{self.BLOG_API}/posts/{post_id}/unpublish"
return self._make_request("POST", endpoint)
# Category Management
def list_categories(self) -> Dict:
"""
List all blog categories.
Returns:
Dictionary containing blog categories
"""
endpoint = f"{self.BLOG_API}/categories"
return self._make_request("GET", endpoint)
def create_category(self, label: str, description: Optional[str] = None) -> Dict:
"""
Create a new blog category.
Args:
label: Category name
description: Category description (optional)
Returns:
Created category data
"""
endpoint = f"{self.BLOG_API}/categories"
payload = {
"category": {
"label": label,
"description": description or ""
}
}
return self._make_request("POST", endpoint, data=payload)
def update_category(
self,
category_id: str,
label: Optional[str] = None,
description: Optional[str] = None
) -> Dict:
"""
Update an existing blog category.
Args:
category_id: ID of the category to update
label: New category name (optional)
description: New category description (optional)
Returns:
Updated category data
"""
# First, get the current category data
current_categories = self.list_categories()
current_category = None
for category in current_categories.get("categories", []):
if category.get("id") == category_id:
current_category = category
break
if not current_category:
raise ValueError(f"Category with ID {category_id} not found")
# Update only the fields that were provided
update_data = {
"category": {
"id": category_id,
"label": label if label is not None else current_category.get("label", ""),
"description": description if description is not None else current_category.get("description", "")
}
}
endpoint = f"{self.BLOG_API}/categories/{category_id}"
return self._make_request("PATCH", endpoint, data=update_data)
def delete_category(self, category_id: str) -> Dict:
"""
Delete a blog category.
Args:
category_id: ID of the category to delete
Returns:
Response data
"""
endpoint = f"{self.BLOG_API}/categories/{category_id}"
return self._make_request("DELETE", endpoint)
# Media Management
def upload_image(
self,
file_path: str,
title: Optional[str] = None,
alt_text: Optional[str] = None,
description: Optional[str] = None
) -> Dict:
"""
Upload an image to the Wix media manager.
Args:
file_path: Path to the image file
title: Image title (optional)
alt_text: Image alt text for accessibility (optional)
description: Image description (optional)
Returns:
Uploaded image data
"""
# Check if file exists
if not os.path.isfile(file_path):
raise FileNotFoundError(f"File not found: {file_path}")
# Get file name and mime type
file_name = os.path.basename(file_path)
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type or not mime_type.startswith('image/'):
raise ValueError(f"File does not appear to be an image: {file_path}")
# Prepare metadata
metadata = {
"title": title or file_name,
"altText": alt_text or "",
"description": description or ""
}
# First, get an upload URL
endpoint = f"{self.MEDIA_API}/files/upload/url"
upload_url_response = self._make_request("POST", endpoint, data={
"mimeType": mime_type,
"fileName": file_name
})
if "uploadUrl" not in upload_url_response:
raise ValueError("Failed to get upload URL")
upload_url = upload_url_response["uploadUrl"]
# Upload the file to the provided URL
with open(file_path, 'rb') as file:
upload_response = requests.post(
upload_url,
files={'file': (file_name, file, mime_type)},
headers={"Content-Type": mime_type}
)
upload_response.raise_for_status()
# Complete the upload with metadata
endpoint = f"{self.MEDIA_API}/files"
complete_data = {
"uploadToken": upload_url_response.get("uploadToken"),
"mediaOptions": {
"mimeType": mime_type,
"fileName": file_name,
"mediaType": "IMAGE",
"title": metadata["title"],
"description": metadata["description"],
"alt": metadata["altText"]
}
}
return self._make_request("POST", endpoint, data=complete_data)
def get_media_item(self, media_id: str) -> Dict:
"""
Get details of a specific media item.
Args:
media_id: ID of the media item
Returns:
Media item data
"""
endpoint = f"{self.MEDIA_API}/files/{media_id}"
return self._make_request("GET", endpoint)
def list_media_items(
self,
media_type: str = "IMAGE",
limit: int = 50,
offset: int = 0
) -> Dict:
"""
List media items with pagination.
Args:
media_type: Type of media to list (IMAGE, VIDEO, AUDIO, DOCUMENT)
limit: Maximum number of items to return
offset: Pagination offset
Returns:
Dictionary containing media items and pagination info
"""
endpoint = f"{self.MEDIA_API}/files/query"
payload = {
"query": {
"paging": {
"limit": limit,
"offset": offset
},
"filter": {
"mediaType": media_type
}
}
}
return self._make_request("POST", endpoint, data=payload)
def delete_media_item(self, media_id: str) -> Dict:
"""
Delete a media item.
Args:
media_id: ID of the media item to delete
Returns:
Response data
"""
endpoint = f"{self.MEDIA_API}/files/{media_id}"
return self._make_request("DELETE", endpoint)
# SEO Management
def get_seo_settings(self, page_url: str) -> Dict:
"""
Get SEO settings for a specific page.
Args:
page_url: URL path of the page (e.g., "/blog/my-post")
Returns:
SEO settings data
"""
endpoint = f"{self.SEO_API}/sites/{self.site_id}/url/{page_url}"
return self._make_request("GET", endpoint)
def update_seo_settings(
self,
page_url: str,
title: Optional[str] = None,
description: Optional[str] = None,
keywords: Optional[List[str]] = None,
og_image_url: Optional[str] = None,
structured_data: Optional[Dict] = None,
no_index: Optional[bool] = None
) -> Dict:
"""
Update SEO settings for a specific page.
Args:
page_url: URL path of the page (e.g., "/blog/my-post")
title: SEO title
description: SEO description
keywords: SEO keywords
og_image_url: Open Graph image URL
structured_data: Structured data (JSON-LD)
no_index: Whether to prevent indexing by search engines
Returns:
Updated SEO settings data
"""
# First, get current SEO settings
try:
current_settings = self.get_seo_settings(page_url)
except:
# If the page doesn't exist yet, start with empty settings
current_settings = {"tags": {}}
# Prepare the update data
seo_data = {
"tags": {}
}
# Update only the fields that were provided
if title is not None:
seo_data["tags"]["title"] = title
elif "title" in current_settings.get("tags", {}):
seo_data["tags"]["title"] = current_settings["tags"]["title"]
if description is not None:
seo_data["tags"]["description"] = description
elif "description" in current_settings.get("tags", {}):
seo_data["tags"]["description"] = current_settings["tags"]["description"]
if keywords is not None:
seo_data["tags"]["keywords"] = ", ".join(keywords)
elif "keywords" in current_settings.get("tags", {}):
seo_data["tags"]["keywords"] = current_settings["tags"]["keywords"]
if og_image_url is not None:
seo_data["tags"]["og:image"] = og_image_url
elif "og:image" in current_settings.get("tags", {}):
seo_data["tags"]["og:image"] = current_settings["tags"]["og:image"]
if structured_data is not None:
seo_data["tags"]["jsonld"] = json.dumps(structured_data)
elif "jsonld" in current_settings.get("tags", {}):
seo_data["tags"]["jsonld"] = current_settings["tags"]["jsonld"]
if no_index is not None:
seo_data["tags"]["robots"] = "noindex" if no_index else "index"
elif "robots" in current_settings.get("tags", {}):
seo_data["tags"]["robots"] = current_settings["tags"]["robots"]
endpoint = f"{self.SEO_API}/sites/{self.site_id}/url/{page_url}"
return self._make_request("PUT", endpoint, data=seo_data)
# Helper Methods
def create_blog_post_with_image(
self,
title: str,
content: str,
image_path: Optional[str] = None,
excerpt: Optional[str] = None,
tags: Optional[List[str]] = None,
categories: Optional[List[str]] = None,
seo_title: Optional[str] = None,
seo_description: Optional[str] = None,
seo_keywords: Optional[List[str]] = None,
publish: bool = False
) -> Dict:
"""
Create a blog post with an optional featured image in one operation.
Args:
title: Post title
content: Post content (HTML)
image_path: Path to featured image (optional)
excerpt: Post excerpt/summary (optional)
tags: List of tags (optional)
categories: List of category IDs (optional)
seo_title: SEO title (optional)
seo_description: SEO description (optional)
seo_keywords: SEO keywords (optional)
publish: Whether to publish the post immediately (optional)
Returns:
Created blog post data
"""
# Upload image if provided
featured_image_id = None
if image_path and os.path.isfile(image_path):
try:
image_response = self.upload_image(
file_path=image_path,
title=title,
alt_text=title
)
featured_image_id = image_response.get("file", {}).get("id")
logger.info(f"Uploaded image with ID: {featured_image_id}")
except Exception as e:
logger.error(f"Failed to upload image: {str(e)}")
# Prepare SEO data
seo_data = None
if seo_title or seo_description or seo_keywords:
seo_data = {
"title": seo_title or title,
"description": seo_description or excerpt or "",
"keywords": seo_keywords or tags or []
}
# Create the blog post
return self.create_post(
title=title,
content=content,
excerpt=excerpt,
featured_image_id=featured_image_id,
tags=tags,
categories=categories,
seo_data=seo_data,
publish=publish
)
def get_or_create_category(self, category_name: str) -> str:
"""
Get a category ID by name, creating it if it doesn't exist.
Args:
category_name: Name of the category
Returns:
Category ID
"""
# List all categories
categories_response = self.list_categories()
categories = categories_response.get("categories", [])
# Check if category exists
for category in categories:
if category.get("label", "").lower() == category_name.lower():
return category.get("id")
# Create category if it doesn't exist
create_response = self.create_category(label=category_name)
return create_response.get("category", {}).get("id")
def get_post_by_slug(self, slug: str) -> Optional[Dict]:
"""
Find a post by its slug.
Args:
slug: Post slug
Returns:
Post data or None if not found
"""
# List posts with a filter for the slug
filter_by = {
"slug": {
"$eq": slug
}
}
response = self.list_posts(limit=1, filter_by=filter_by)
posts = response.get("posts", [])
if posts:
return posts[0]
return None
def get_post_url(self, post_id: str) -> str:
"""
Get the full URL for a blog post.
Args:
post_id: ID of the blog post
Returns:
Full URL to the blog post
"""
post_data = self.get_post(post_id)
slug = post_data.get("post", {}).get("slug", "")
# Get the blog URL prefix
# This is a simplification - in reality, you might need to get this from site settings
return f"/blog/{slug}"

View File

@@ -0,0 +1,720 @@
"""
Wix Blog Manager
This module provides high-level functions for managing blog content on Wix,
including content creation, SEO optimization, and media management.
"""
import os
import re
import logging
import tempfile
import requests
from typing import Dict, List, Optional, Union, Any, Tuple
from datetime import datetime
from pathlib import Path
import markdown
import html2text
from bs4 import BeautifulSoup
from .wix_api_client import WixAPIClient
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('wix_blog_manager')
class WixBlogManager:
"""
High-level manager for Wix blog content.
This class provides convenient methods for common blog management tasks,
building on the lower-level WixAPIClient.
"""
def __init__(
self,
api_key: Optional[str] = None,
refresh_token: Optional[str] = None,
site_id: Optional[str] = None
):
"""
Initialize the Wix Blog Manager.
Args:
api_key: Wix API key (optional if using refresh token)
refresh_token: Wix refresh token for OAuth authentication
site_id: Wix site ID
"""
self.client = WixAPIClient(api_key, refresh_token, site_id)
def publish_markdown_post(
self,
title: str,
markdown_content: str,
featured_image_path: Optional[str] = None,
featured_image_url: Optional[str] = None,
excerpt: Optional[str] = None,
tags: Optional[List[str]] = None,
categories: Optional[List[str]] = None,
seo_title: Optional[str] = None,
seo_description: Optional[str] = None,
seo_keywords: Optional[List[str]] = None,
publish: bool = False
) -> Dict:
"""
Publish a blog post from markdown content.
Args:
title: Post title
markdown_content: Post content in markdown format
featured_image_path: Local path to featured image (optional)
featured_image_url: URL of featured image to download (optional)
excerpt: Post excerpt/summary (optional)
tags: List of tags (optional)
categories: List of category names (optional)
seo_title: SEO title (optional)
seo_description: SEO description (optional)
seo_keywords: SEO keywords (optional)
publish: Whether to publish the post immediately (optional)
Returns:
Published blog post data
"""
# Convert markdown to HTML
html_content = self._markdown_to_html(markdown_content)
# Process images in the content
html_content, embedded_images = self._process_content_images(html_content)
# Handle featured image
featured_image_id = None
temp_image_path = None
if featured_image_url and not featured_image_path:
# Download the image from URL
try:
temp_image_path = self._download_image(featured_image_url)
featured_image_path = temp_image_path
except Exception as e:
logger.error(f"Failed to download featured image: {str(e)}")
if featured_image_path:
try:
image_response = self.client.upload_image(
file_path=featured_image_path,
title=title,
alt_text=title
)
featured_image_id = image_response.get("file", {}).get("id")
logger.info(f"Uploaded featured image with ID: {featured_image_id}")
except Exception as e:
logger.error(f"Failed to upload featured image: {str(e)}")
# Clean up temporary file if created
if temp_image_path and os.path.exists(temp_image_path):
try:
os.remove(temp_image_path)
except:
pass
# Process categories - convert names to IDs
category_ids = []
if categories:
for category_name in categories:
try:
category_id = self.client.get_or_create_category(category_name)
if category_id:
category_ids.append(category_id)
except Exception as e:
logger.error(f"Failed to process category '{category_name}': {str(e)}")
# Generate excerpt if not provided
if not excerpt:
excerpt = self._generate_excerpt(markdown_content)
# Prepare SEO data
seo_data = None
if seo_title or seo_description or seo_keywords:
seo_data = {
"title": seo_title or title,
"description": seo_description or excerpt or "",
"keywords": seo_keywords or tags or []
}
# Create the blog post
response = self.client.create_post(
title=title,
content=html_content,
excerpt=excerpt,
featured_image_id=featured_image_id,
tags=tags,
categories=category_ids,
seo_data=seo_data,
publish=publish
)
# Update SEO settings if the post was published
if publish and response.get("post", {}).get("id"):
post_id = response["post"]["id"]
post_url = self.client.get_post_url(post_id)
try:
self.client.update_seo_settings(
page_url=post_url,
title=seo_title or title,
description=seo_description or excerpt or "",
keywords=seo_keywords or tags,
og_image_url=featured_image_url
)
except Exception as e:
logger.error(f"Failed to update SEO settings: {str(e)}")
return response
def update_markdown_post(
self,
post_id: str,
title: Optional[str] = None,
markdown_content: Optional[str] = None,
featured_image_path: Optional[str] = None,
featured_image_url: Optional[str] = None,
excerpt: Optional[str] = None,
tags: Optional[List[str]] = None,
categories: Optional[List[str]] = None,
seo_title: Optional[str] = None,
seo_description: Optional[str] = None,
seo_keywords: Optional[List[str]] = None,
publish: bool = False
) -> Dict:
"""
Update an existing blog post with markdown content.
Args:
post_id: ID of the post to update
title: New post title (optional)
markdown_content: New post content in markdown format (optional)
featured_image_path: Local path to new featured image (optional)
featured_image_url: URL of new featured image to download (optional)
excerpt: New post excerpt/summary (optional)
tags: New list of tags (optional)
categories: New list of category names (optional)
seo_title: New SEO title (optional)
seo_description: New SEO description (optional)
seo_keywords: New SEO keywords (optional)
publish: Whether to publish the post after updating (optional)
Returns:
Updated blog post data
"""
# Get current post data
current_post = self.client.get_post(post_id)
if "post" not in current_post:
raise ValueError(f"Post with ID {post_id} not found")
# Convert markdown to HTML if provided
html_content = None
if markdown_content:
html_content = self._markdown_to_html(markdown_content)
# Process images in the content
html_content, embedded_images = self._process_content_images(html_content)
# Handle featured image
featured_image_id = None
temp_image_path = None
if featured_image_url and not featured_image_path:
# Download the image from URL
try:
temp_image_path = self._download_image(featured_image_url)
featured_image_path = temp_image_path
except Exception as e:
logger.error(f"Failed to download featured image: {str(e)}")
if featured_image_path:
try:
image_response = self.client.upload_image(
file_path=featured_image_path,
title=title or current_post["post"].get("title", ""),
alt_text=title or current_post["post"].get("title", "")
)
featured_image_id = image_response.get("file", {}).get("id")
logger.info(f"Uploaded featured image with ID: {featured_image_id}")
except Exception as e:
logger.error(f"Failed to upload featured image: {str(e)}")
# Clean up temporary file if created
if temp_image_path and os.path.exists(temp_image_path):
try:
os.remove(temp_image_path)
except:
pass
# Process categories - convert names to IDs
category_ids = None
if categories:
category_ids = []
for category_name in categories:
try:
category_id = self.client.get_or_create_category(category_name)
if category_id:
category_ids.append(category_id)
except Exception as e:
logger.error(f"Failed to process category '{category_name}': {str(e)}")
# Generate excerpt if not provided but markdown is
if not excerpt and markdown_content:
excerpt = self._generate_excerpt(markdown_content)
# Prepare SEO data
seo_data = None
if seo_title or seo_description or seo_keywords:
seo_data = {
"title": seo_title or title or current_post["post"].get("title", ""),
"description": seo_description or excerpt or current_post["post"].get("excerpt", ""),
"keywords": seo_keywords or tags or current_post["post"].get("tags", [])
}
# Update the blog post
response = self.client.update_post(
post_id=post_id,
title=title,
content=html_content,
excerpt=excerpt,
featured_image_id=featured_image_id,
tags=tags,
categories=category_ids,
seo_data=seo_data,
publish=publish
)
# Update SEO settings if needed
if (seo_title or seo_description or seo_keywords or featured_image_url):
post_url = self.client.get_post_url(post_id)
try:
self.client.update_seo_settings(
page_url=post_url,
title=seo_title or title,
description=seo_description or excerpt,
keywords=seo_keywords or tags,
og_image_url=featured_image_url
)
except Exception as e:
logger.error(f"Failed to update SEO settings: {str(e)}")
return response
def find_post_by_title(self, title: str) -> Optional[Dict]:
"""
Find a post by its title (exact match).
Args:
title: Post title to search for
Returns:
Post data or None if not found
"""
# List all posts (this is inefficient but Wix API doesn't support filtering by title)
# In a production environment, you might want to implement pagination
response = self.client.list_posts(limit=100)
posts = response.get("posts", [])
for post in posts:
if post.get("title") == title:
return post
return None
def publish_or_update_markdown_post(
self,
title: str,
markdown_content: str,
featured_image_path: Optional[str] = None,
featured_image_url: Optional[str] = None,
excerpt: Optional[str] = None,
tags: Optional[List[str]] = None,
categories: Optional[List[str]] = None,
seo_title: Optional[str] = None,
seo_description: Optional[str] = None,
seo_keywords: Optional[List[str]] = None,
publish: bool = False,
update_if_exists: bool = True
) -> Dict:
"""
Publish a new post or update an existing one with the same title.
Args:
title: Post title
markdown_content: Post content in markdown format
featured_image_path: Local path to featured image (optional)
featured_image_url: URL of featured image to download (optional)
excerpt: Post excerpt/summary (optional)
tags: List of tags (optional)
categories: List of category names (optional)
seo_title: SEO title (optional)
seo_description: SEO description (optional)
seo_keywords: SEO keywords (optional)
publish: Whether to publish the post immediately (optional)
update_if_exists: Whether to update an existing post with the same title (optional)
Returns:
Published or updated blog post data
"""
# Check if a post with this title already exists
existing_post = self.find_post_by_title(title)
if existing_post and update_if_exists:
# Update existing post
logger.info(f"Updating existing post with title: {title}")
return self.update_markdown_post(
post_id=existing_post["id"],
title=title,
markdown_content=markdown_content,
featured_image_path=featured_image_path,
featured_image_url=featured_image_url,
excerpt=excerpt,
tags=tags,
categories=categories,
seo_title=seo_title,
seo_description=seo_description,
seo_keywords=seo_keywords,
publish=publish
)
else:
# Create new post
logger.info(f"Creating new post with title: {title}")
return self.publish_markdown_post(
title=title,
markdown_content=markdown_content,
featured_image_path=featured_image_path,
featured_image_url=featured_image_url,
excerpt=excerpt,
tags=tags,
categories=categories,
seo_title=seo_title,
seo_description=seo_description,
seo_keywords=seo_keywords,
publish=publish
)
def optimize_seo_for_post(
self,
post_id: str,
seo_title: Optional[str] = None,
seo_description: Optional[str] = None,
seo_keywords: Optional[List[str]] = None,
og_image_url: Optional[str] = None,
structured_data: Optional[Dict] = None
) -> Dict:
"""
Optimize SEO settings for an existing blog post.
Args:
post_id: ID of the blog post
seo_title: SEO title (optional)
seo_description: SEO description (optional)
seo_keywords: SEO keywords (optional)
og_image_url: Open Graph image URL (optional)
structured_data: Structured data (JSON-LD) (optional)
Returns:
Updated SEO settings data
"""
# Get the post URL
post_url = self.client.get_post_url(post_id)
# Update SEO settings
return self.client.update_seo_settings(
page_url=post_url,
title=seo_title,
description=seo_description,
keywords=seo_keywords,
og_image_url=og_image_url,
structured_data=structured_data
)
def generate_structured_data(
self,
post_id: str,
author_name: str,
publisher_name: str,
publisher_logo_url: str
) -> Dict:
"""
Generate structured data (JSON-LD) for a blog post.
Args:
post_id: ID of the blog post
author_name: Name of the author
publisher_name: Name of the publisher
publisher_logo_url: URL of the publisher's logo
Returns:
Structured data as a dictionary
"""
# Get post data
post_data = self.client.get_post(post_id)
post = post_data.get("post", {})
# Get post URL
post_url = self.client.get_post_url(post_id)
# Create structured data
structured_data = {
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": post.get("title", ""),
"description": post.get("excerpt", ""),
"author": {
"@type": "Person",
"name": author_name
},
"publisher": {
"@type": "Organization",
"name": publisher_name,
"logo": {
"@type": "ImageObject",
"url": publisher_logo_url
}
},
"datePublished": post.get("publishedDate", ""),
"dateModified": post.get("lastPublishedDate", "")
}
# Add featured image if available
if post.get("featuredImageId"):
try:
media_item = self.client.get_media_item(post["featuredImageId"])
image_url = media_item.get("file", {}).get("url", "")
if image_url:
structured_data["image"] = image_url
except:
pass
return structured_data
def apply_structured_data_to_post(
self,
post_id: str,
author_name: str,
publisher_name: str,
publisher_logo_url: str
) -> Dict:
"""
Generate and apply structured data to a blog post.
Args:
post_id: ID of the blog post
author_name: Name of the author
publisher_name: Name of the publisher
publisher_logo_url: URL of the publisher's logo
Returns:
Updated SEO settings data
"""
# Generate structured data
structured_data = self.generate_structured_data(
post_id=post_id,
author_name=author_name,
publisher_name=publisher_name,
publisher_logo_url=publisher_logo_url
)
# Get the post URL
post_url = self.client.get_post_url(post_id)
# Update SEO settings with structured data
return self.client.update_seo_settings(
page_url=post_url,
structured_data=structured_data
)
# Helper methods
def _markdown_to_html(self, markdown_content: str) -> str:
"""
Convert markdown content to HTML.
Args:
markdown_content: Content in markdown format
Returns:
HTML content
"""
# Use the markdown library to convert to HTML
html = markdown.markdown(
markdown_content,
extensions=['extra', 'codehilite', 'tables', 'toc']
)
return html
def _html_to_markdown(self, html_content: str) -> str:
"""
Convert HTML content to markdown.
Args:
html_content: Content in HTML format
Returns:
Markdown content
"""
# Use html2text to convert HTML to markdown
h = html2text.HTML2Text()
h.ignore_links = False
h.ignore_images = False
h.ignore_tables = False
h.ignore_emphasis = False
return h.handle(html_content)
def _process_content_images(self, html_content: str) -> Tuple[str, List[Dict]]:
"""
Process images in HTML content, uploading them to Wix and replacing URLs.
Args:
html_content: HTML content with image tags
Returns:
Tuple of (updated HTML content, list of uploaded image data)
"""
soup = BeautifulSoup(html_content, 'html.parser')
img_tags = soup.find_all('img')
uploaded_images = []
for img in img_tags:
src = img.get('src', '')
alt = img.get('alt', '')
# Skip images that are already hosted on Wix
if 'wixstatic.com' in src:
continue
# Handle images with data URLs
if src.startswith('data:image'):
logger.info("Skipping data URL image - not supported in this implementation")
continue
# Handle remote images
if src.startswith('http://') or src.startswith('https://'):
try:
# Download the image
temp_path = self._download_image(src)
# Upload to Wix
image_response = self.client.upload_image(
file_path=temp_path,
title=alt or "Blog image",
alt_text=alt or "Blog image"
)
# Get the new URL
new_url = image_response.get("file", {}).get("url", "")
if new_url:
# Replace the src attribute
img['src'] = new_url
uploaded_images.append({
'original_url': src,
'wix_url': new_url,
'wix_id': image_response.get("file", {}).get("id", "")
})
# Clean up temp file
if os.path.exists(temp_path):
os.remove(temp_path)
except Exception as e:
logger.error(f"Failed to process image {src}: {str(e)}")
# Handle local images (not implemented in this version)
else:
logger.info(f"Skipping local image {src} - not supported in this implementation")
# Return the updated HTML
return str(soup), uploaded_images
def _download_image(self, url: str) -> str:
"""
Download an image from a URL to a temporary file.
Args:
url: URL of the image
Returns:
Path to the downloaded temporary file
"""
response = requests.get(url, stream=True)
response.raise_for_status()
# Determine file extension
content_type = response.headers.get('content-type', '')
extension = '.jpg' # Default
if 'image/jpeg' in content_type:
extension = '.jpg'
elif 'image/png' in content_type:
extension = '.png'
elif 'image/gif' in content_type:
extension = '.gif'
elif 'image/webp' in content_type:
extension = '.webp'
# Create a temporary file
fd, temp_path = tempfile.mkstemp(suffix=extension)
os.close(fd)
# Write the image data to the file
with open(temp_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
return temp_path
def _generate_excerpt(self, markdown_content: str, max_length: int = 160) -> str:
"""
Generate an excerpt from markdown content.
Args:
markdown_content: Content in markdown format
max_length: Maximum length of the excerpt
Returns:
Generated excerpt
"""
# Convert markdown to plain text
h = html2text.HTML2Text()
h.ignore_links = True
h.ignore_images = True
h.ignore_tables = True
h.ignore_emphasis = True
# First convert markdown to HTML, then HTML to plain text
html = markdown.markdown(markdown_content)
plain_text = h.handle(html)
# Clean up the text
plain_text = re.sub(r'\s+', ' ', plain_text).strip()
# Truncate to max_length
if len(plain_text) <= max_length:
return plain_text
# Try to truncate at a sentence boundary
sentences = re.split(r'(?<=[.!?])\s+', plain_text)
excerpt = ""
for sentence in sentences:
if len(excerpt + sentence) <= max_length:
excerpt += sentence + " "
else:
break
# If we couldn't get a full sentence, just truncate
if not excerpt:
excerpt = plain_text[:max_length-3] + "..."
return excerpt.strip()

View File

@@ -0,0 +1,350 @@
"""
Wix Blog Publisher for Alwrity
This module integrates the Wix API with the Alwrity AI Writer platform,
allowing users to publish generated blog content directly to their Wix site.
"""
import os
import logging
import tempfile
import streamlit as st
from typing import Dict, List, Optional, Union, Any, Tuple
from pathlib import Path
from .wix_integration import WixIntegration
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('wix_blog_publisher')
def publish_to_wix(
title: str,
content: str,
is_markdown: bool = True,
featured_image_path: Optional[str] = None,
featured_image_url: Optional[str] = None,
excerpt: Optional[str] = None,
tags: Optional[List[str]] = None,
categories: Optional[List[str]] = None,
seo_title: Optional[str] = None,
seo_description: Optional[str] = None,
seo_keywords: Optional[List[str]] = None,
author_name: Optional[str] = None,
publisher_name: Optional[str] = None,
publisher_logo_url: Optional[str] = None,
publish: bool = True,
update_if_exists: bool = True,
api_key: Optional[str] = None,
refresh_token: Optional[str] = None,
site_id: Optional[str] = None
) -> Dict:
"""
Publish a blog post to Wix.
Args:
title: Post title
content: Post content (markdown or HTML)
is_markdown: Whether the content is in markdown format
featured_image_path: Local path to featured image (optional)
featured_image_url: URL of featured image to download (optional)
excerpt: Post excerpt/summary (optional)
tags: List of tags (optional)
categories: List of category names (optional)
seo_title: SEO title (optional)
seo_description: SEO description (optional)
seo_keywords: SEO keywords (optional)
author_name: Name of the author (optional)
publisher_name: Name of the publisher (optional)
publisher_logo_url: URL of the publisher's logo (optional)
publish: Whether to publish the post immediately (optional)
update_if_exists: Whether to update an existing post with the same title (optional)
api_key: Wix API key (optional if using refresh token)
refresh_token: Wix refresh token for OAuth authentication
site_id: Wix site ID
Returns:
Published blog post data
"""
# Initialize Wix integration
wix = WixIntegration(api_key, refresh_token, site_id)
# Publish the blog post
return wix.publish_blog_post(
title=title,
content=content,
is_markdown=is_markdown,
featured_image_path=featured_image_path,
featured_image_url=featured_image_url,
excerpt=excerpt,
tags=tags,
categories=categories,
seo_title=seo_title,
seo_description=seo_description,
seo_keywords=seo_keywords,
author_name=author_name,
publisher_name=publisher_name,
publisher_logo_url=publisher_logo_url,
publish=publish,
update_if_exists=update_if_exists
)
def wix_blog_publisher_ui():
"""
Streamlit UI for publishing blog posts to Wix.
"""
st.title("Publish to Wix")
st.write("Publish your blog content directly to your Wix site.")
# Authentication settings
st.header("Wix Authentication")
# Check for saved credentials
if "wix_refresh_token" in st.session_state and "wix_site_id" in st.session_state:
st.success("✅ Wix credentials are saved in this session.")
show_saved = st.checkbox("Show saved credentials")
if show_saved:
st.text_input("Refresh Token", value=st.session_state.wix_refresh_token, type="password", disabled=True)
st.text_input("Site ID", value=st.session_state.wix_site_id, disabled=True)
clear_creds = st.button("Clear saved credentials")
if clear_creds:
if "wix_refresh_token" in st.session_state:
del st.session_state.wix_refresh_token
if "wix_site_id" in st.session_state:
del st.session_state.wix_site_id
st.rerun()
else:
col1, col2 = st.columns(2)
with col1:
refresh_token = st.text_input("Wix Refresh Token", type="password", help="Your Wix refresh token for API authentication")
with col2:
site_id = st.text_input("Wix Site ID", help="Your Wix site ID")
save_creds = st.checkbox("Save credentials for this session", value=True)
if st.button("Validate Credentials"):
if not refresh_token:
st.error("Refresh token is required.")
return
if not site_id:
st.error("Site ID is required.")
return
# Try to initialize Wix integration to validate credentials
try:
wix = WixIntegration(refresh_token=refresh_token, site_id=site_id)
# Test API call
site_info = wix.get_site_info()
if site_info.get("status") == "connected":
st.success(f"✅ Credentials validated successfully! Found {site_info.get('post_count', 0)} posts and {site_info.get('category_count', 0)} categories.")
# Save credentials if requested
if save_creds:
st.session_state.wix_refresh_token = refresh_token
st.session_state.wix_site_id = site_id
st.rerun()
else:
st.error(f"❌ Failed to validate credentials: {site_info.get('error', 'Unknown error')}")
except Exception as e:
st.error(f"❌ Failed to validate credentials: {str(e)}")
return
# Blog content section
st.header("Blog Content")
# Check if we have content in session state (from other parts of the app)
blog_title = st.text_input(
"Blog Title",
value=st.session_state.get("blog_title", ""),
help="The title of your blog post"
)
content_type = st.radio(
"Content Format",
["Markdown", "HTML"],
horizontal=True,
help="The format of your blog content"
)
is_markdown = content_type == "Markdown"
blog_content = st.text_area(
"Blog Content",
value=st.session_state.get("blog_content", ""),
height=300,
help="The content of your blog post"
)
# Featured image
st.subheader("Featured Image")
image_source = st.radio(
"Image Source",
["None", "Upload", "URL"],
horizontal=True,
help="How to provide the featured image"
)
featured_image_path = None
featured_image_url = None
if image_source == "Upload":
uploaded_file = st.file_uploader("Upload Featured Image", type=["jpg", "jpeg", "png", "gif"])
if uploaded_file:
# Save the uploaded file to a temporary location
with tempfile.NamedTemporaryFile(delete=False, suffix=f".{uploaded_file.name.split('.')[-1]}") as tmp:
tmp.write(uploaded_file.getvalue())
featured_image_path = tmp.name
elif image_source == "URL":
featured_image_url = st.text_input("Featured Image URL", help="URL of the featured image")
# Blog metadata
st.header("Blog Metadata")
col1, col2 = st.columns(2)
with col1:
excerpt = st.text_area(
"Excerpt",
value=st.session_state.get("blog_excerpt", ""),
help="A short summary of your blog post"
)
tags_input = st.text_input(
"Tags (comma-separated)",
value=", ".join(st.session_state.get("blog_tags", [])) if isinstance(st.session_state.get("blog_tags", []), list) else st.session_state.get("blog_tags", ""),
help="Tags for your blog post, separated by commas"
)
tags = [tag.strip() for tag in tags_input.split(",")] if tags_input else None
categories_input = st.text_input(
"Categories (comma-separated)",
value=", ".join(st.session_state.get("blog_categories", [])) if isinstance(st.session_state.get("blog_categories", []), list) else st.session_state.get("blog_categories", ""),
help="Categories for your blog post, separated by commas"
)
categories = [cat.strip() for cat in categories_input.split(",")] if categories_input else None
with col2:
author_name = st.text_input("Author Name", help="Name of the blog post author")
publisher_name = st.text_input("Publisher Name", help="Name of the blog publisher (usually your site name)")
publisher_logo_url = st.text_input("Publisher Logo URL", help="URL of the publisher's logo")
# SEO settings
with st.expander("SEO Settings"):
seo_title = st.text_input("SEO Title", value=blog_title, help="Title for search engines (defaults to blog title)")
seo_description = st.text_area("SEO Description", value=excerpt, help="Description for search engines (defaults to excerpt)")
seo_keywords_input = st.text_input("SEO Keywords (comma-separated)", value=tags_input, help="Keywords for search engines (defaults to tags)")
seo_keywords = [kw.strip() for kw in seo_keywords_input.split(",")] if seo_keywords_input else None
# Publishing options
st.header("Publishing Options")
col1, col2 = st.columns(2)
with col1:
publish = not st.checkbox("Save as draft", help="If checked, the post will be saved as a draft instead of being published")
with col2:
update_if_exists = st.checkbox("Update if exists", value=True, help="If checked, an existing post with the same title will be updated")
# Publish button
if st.button("Publish to Wix", type="primary"):
if not blog_title:
st.error("Blog title is required.")
return
if not blog_content:
st.error("Blog content is required.")
return
# Get credentials
refresh_token = st.session_state.get("wix_refresh_token")
site_id = st.session_state.get("wix_site_id")
if not refresh_token or not site_id:
st.error("Wix credentials are required. Please enter them in the authentication section.")
return
# Show progress
with st.spinner("Publishing to Wix..."):
try:
# Publish to Wix
result = publish_to_wix(
title=blog_title,
content=blog_content,
is_markdown=is_markdown,
featured_image_path=featured_image_path,
featured_image_url=featured_image_url,
excerpt=excerpt,
tags=tags,
categories=categories,
seo_title=seo_title,
seo_description=seo_description,
seo_keywords=seo_keywords,
author_name=author_name,
publisher_name=publisher_name,
publisher_logo_url=publisher_logo_url,
publish=publish,
update_if_exists=update_if_exists,
refresh_token=refresh_token,
site_id=site_id
)
# Clean up temporary file if created
if featured_image_path and os.path.exists(featured_image_path) and featured_image_path.startswith(tempfile.gettempdir()):
try:
os.remove(featured_image_path)
except:
pass
# Show success message
st.success("✅ Blog post published successfully!")
# Show post details
post = result.get("post", {})
st.subheader("Published Post Details")
col1, col2 = st.columns(2)
with col1:
st.write(f"**Title:** {post.get('title', 'N/A')}")
st.write(f"**Status:** {post.get('status', 'N/A')}")
st.write(f"**ID:** {post.get('id', 'N/A')}")
with col2:
st.write(f"**Published Date:** {post.get('publishedDate', 'N/A')}")
st.write(f"**URL:** {post.get('url', 'N/A')}")
st.write(f"**Tags:** {', '.join(post.get('tags', []))}")
# Add a view button if URL is available
if post.get("url"):
st.markdown(f"[View Post]({post.get('url')})")
# Add SEO report button
if st.button("Generate SEO Report"):
with st.spinner("Generating SEO report..."):
try:
wix = WixIntegration(refresh_token=refresh_token, site_id=site_id)
seo_report = wix.get_seo_report(post.get("id"), seo_keywords or tags or [])
st.subheader("SEO Report")
st.write(f"**SEO Score:** {seo_report.get('seo_score', 0):.1f}/100")
st.write("**Recommendations:**")
for i, rec in enumerate(seo_report.get("recommendations", [])):
st.write(f"{i+1}. {rec}")
except Exception as e:
st.error(f"Failed to generate SEO report: {str(e)}")
except Exception as e:
st.error(f"❌ Failed to publish blog post: {str(e)}")
logger.error(f"Failed to publish blog post: {str(e)}")
# For testing the UI directly
if __name__ == "__main__":
wix_blog_publisher_ui()

View File

@@ -0,0 +1,388 @@
"""
Wix Integration for Alwrity
This module provides a high-level interface for integrating Wix blog functionality
with the Alwrity AI Writer platform.
"""
import os
import logging
import json
from typing import Dict, List, Optional, Union, Any, Tuple
from pathlib import Path
from .wix_api_client import WixAPIClient
from .wix_blog_manager import WixBlogManager
from .wix_seo_optimizer import WixSEOOptimizer
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('wix_integration')
class WixIntegration:
"""
Main integration class for Wix blog functionality.
This class provides a simplified interface for common operations,
combining the functionality of the API client, blog manager, and SEO optimizer.
"""
def __init__(
self,
api_key: Optional[str] = None,
refresh_token: Optional[str] = None,
site_id: Optional[str] = None
):
"""
Initialize the Wix Integration.
Args:
api_key: Wix API key (optional if using refresh token)
refresh_token: Wix refresh token for OAuth authentication
site_id: Wix site ID
"""
self.api_client = WixAPIClient(api_key, refresh_token, site_id)
self.blog_manager = WixBlogManager(api_key, refresh_token, site_id)
self.seo_optimizer = WixSEOOptimizer(api_key, refresh_token, site_id)
def publish_blog_post(
self,
title: str,
content: str,
is_markdown: bool = True,
featured_image_path: Optional[str] = None,
featured_image_url: Optional[str] = None,
excerpt: Optional[str] = None,
tags: Optional[List[str]] = None,
categories: Optional[List[str]] = None,
seo_title: Optional[str] = None,
seo_description: Optional[str] = None,
seo_keywords: Optional[List[str]] = None,
author_name: Optional[str] = None,
publisher_name: Optional[str] = None,
publisher_logo_url: Optional[str] = None,
publish: bool = True,
update_if_exists: bool = True
) -> Dict:
"""
Publish a blog post with comprehensive SEO optimization.
Args:
title: Post title
content: Post content (markdown or HTML)
is_markdown: Whether the content is in markdown format
featured_image_path: Local path to featured image (optional)
featured_image_url: URL of featured image to download (optional)
excerpt: Post excerpt/summary (optional)
tags: List of tags (optional)
categories: List of category names (optional)
seo_title: SEO title (optional)
seo_description: SEO description (optional)
seo_keywords: SEO keywords (optional)
author_name: Name of the author (optional)
publisher_name: Name of the publisher (optional)
publisher_logo_url: URL of the publisher's logo (optional)
publish: Whether to publish the post immediately (optional)
update_if_exists: Whether to update an existing post with the same title (optional)
Returns:
Published blog post data
"""
# Generate SEO data if not provided
if not seo_keywords and tags:
seo_keywords = tags
if not seo_title:
seo_title = title
if not seo_description and not excerpt:
if is_markdown:
# Generate description from markdown content
seo_description = self.blog_manager._generate_excerpt(content)
else:
# Generate description from HTML content
seo_description = self.seo_optimizer.generate_meta_description(content)
elif not seo_description:
seo_description = excerpt
# Publish or update the post
if is_markdown:
response = self.blog_manager.publish_or_update_markdown_post(
title=title,
markdown_content=content,
featured_image_path=featured_image_path,
featured_image_url=featured_image_url,
excerpt=excerpt,
tags=tags,
categories=categories,
seo_title=seo_title,
seo_description=seo_description,
seo_keywords=seo_keywords,
publish=publish,
update_if_exists=update_if_exists
)
else:
# Find existing post or create new one
existing_post = self.blog_manager.find_post_by_title(title)
if existing_post and update_if_exists:
# Update existing post
response = self.api_client.update_post(
post_id=existing_post["id"],
title=title,
content=content,
excerpt=excerpt,
tags=tags,
categories=[self.api_client.get_or_create_category(cat) for cat in categories] if categories else None,
seo_data={
"title": seo_title,
"description": seo_description,
"keywords": seo_keywords or []
},
publish=publish
)
else:
# Create new post
response = self.api_client.create_post(
title=title,
content=content,
excerpt=excerpt,
tags=tags,
categories=[self.api_client.get_or_create_category(cat) for cat in categories] if categories else None,
seo_data={
"title": seo_title,
"description": seo_description,
"keywords": seo_keywords or []
},
publish=publish
)
# Apply additional SEO optimization if the post was published
if publish and response.get("post", {}).get("id"):
post_id = response["post"]["id"]
# Apply structured data if author and publisher info is provided
if author_name and publisher_name and publisher_logo_url:
try:
self.seo_optimizer.apply_structured_data_to_post(
post_id=post_id,
author_name=author_name,
publisher_name=publisher_name,
publisher_logo_url=publisher_logo_url
)
except Exception as e:
logger.error(f"Failed to apply structured data: {str(e)}")
# Apply comprehensive SEO optimization
try:
self.seo_optimizer.apply_seo_optimization(
post_id=post_id,
title=seo_title,
description=seo_description,
keywords=seo_keywords,
author_name=author_name,
publisher_name=publisher_name,
publisher_logo_url=publisher_logo_url,
og_image_url=featured_image_url
)
except Exception as e:
logger.error(f"Failed to apply SEO optimization: {str(e)}")
return response
def upload_media(
self,
file_path: str,
title: Optional[str] = None,
alt_text: Optional[str] = None,
description: Optional[str] = None
) -> Dict:
"""
Upload a media file to Wix.
Args:
file_path: Path to the media file
title: Media title (optional)
alt_text: Media alt text (optional)
description: Media description (optional)
Returns:
Uploaded media data
"""
return self.api_client.upload_image(
file_path=file_path,
title=title,
alt_text=alt_text,
description=description
)
def get_seo_report(self, post_id: str, target_keywords: List[str]) -> Dict:
"""
Generate a comprehensive SEO report for a blog post.
Args:
post_id: ID of the blog post
target_keywords: List of target keywords
Returns:
Dictionary with SEO report data
"""
return self.seo_optimizer.generate_seo_report(post_id, target_keywords)
def list_blog_posts(
self,
limit: int = 50,
offset: int = 0,
sort_field: str = "lastPublishedDate",
sort_order: str = "desc"
) -> Dict:
"""
List blog posts with pagination and sorting.
Args:
limit: Maximum number of posts to return (default: 50)
offset: Pagination offset (default: 0)
sort_field: Field to sort by (default: lastPublishedDate)
sort_order: Sort order, 'asc' or 'desc' (default: desc)
Returns:
Dictionary containing blog posts and pagination info
"""
return self.api_client.list_posts(
limit=limit,
offset=offset,
sort_field=sort_field,
sort_order=sort_order
)
def list_categories(self) -> Dict:
"""
List all blog categories.
Returns:
Dictionary containing blog categories
"""
return self.api_client.list_categories()
def create_category(self, name: str, description: Optional[str] = None) -> str:
"""
Create a new blog category.
Args:
name: Category name
description: Category description (optional)
Returns:
ID of the created category
"""
response = self.api_client.create_category(
label=name,
description=description
)
return response.get("category", {}).get("id", "")
def get_post_by_id(self, post_id: str) -> Dict:
"""
Get a blog post by ID.
Args:
post_id: ID of the blog post
Returns:
Blog post data
"""
return self.api_client.get_post(post_id)
def get_post_by_title(self, title: str) -> Optional[Dict]:
"""
Get a blog post by title.
Args:
title: Title of the blog post
Returns:
Blog post data or None if not found
"""
return self.blog_manager.find_post_by_title(title)
def delete_post(self, post_id: str) -> Dict:
"""
Delete a blog post.
Args:
post_id: ID of the blog post
Returns:
Response data
"""
return self.api_client.delete_post(post_id)
def update_post_status(self, post_id: str, publish: bool = True) -> Dict:
"""
Update the publication status of a blog post.
Args:
post_id: ID of the blog post
publish: Whether to publish (True) or unpublish (False) the post
Returns:
Updated blog post data
"""
if publish:
return self.api_client.publish_post(post_id)
else:
return self.api_client.unpublish_post(post_id)
def search_posts(self, query: str, limit: int = 10) -> List[Dict]:
"""
Search for blog posts by content or title.
Args:
query: Search query
limit: Maximum number of results to return
Returns:
List of matching blog posts
"""
# First try to find by title
title_matches = []
try:
all_posts = self.list_blog_posts(limit=100)["posts"]
for post in all_posts:
if query.lower() in post.get("title", "").lower():
title_matches.append(post)
if len(title_matches) >= limit:
break
except Exception as e:
logger.error(f"Error searching posts by title: {str(e)}")
return title_matches[:limit]
def get_site_info(self) -> Dict:
"""
Get information about the Wix site.
Returns:
Dictionary with site information
"""
try:
# Make a simple API call to verify credentials and get site info
posts = self.list_blog_posts(limit=1)
categories = self.list_categories()
return {
"site_id": self.api_client.site_id,
"post_count": posts.get("totalCount", 0),
"category_count": len(categories.get("categories", [])),
"status": "connected"
}
except Exception as e:
logger.error(f"Error getting site info: {str(e)}")
return {
"site_id": self.api_client.site_id,
"status": "error",
"error": str(e)
}

View File

@@ -8,7 +8,7 @@ from lib.ai_seo_tools.optimize_images_for_upload import main_img_optimizer
from lib.ai_seo_tools.google_pagespeed_insights import google_pagespeed_insights
from lib.ai_seo_tools.on_page_seo_analyzer import analyze_onpage_seo
from lib.ai_seo_tools.weburl_seo_checker import url_seo_checker
from lib.ai_marketing_tools.backlinking_ui_streamlit import backlinking_ui
from lib.ai_marketing_tools.ai_backlinker.backlinking_ui_streamlit import backlinking_ui
def ai_seo_tools():