AI Backlinker, Google Ads Generator, Letter Writer - WIP
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
370
lib/ai_marketing_tools/ai_google_ads_generator/README.md
Normal file
370
lib/ai_marketing_tools/ai_google_ads_generator/README.md
Normal 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
|
||||
@@ -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"]
|
||||
327
lib/ai_marketing_tools/ai_google_ads_generator/ad_analyzer.py
Normal file
327
lib/ai_marketing_tools/ai_google_ads_generator/ad_analyzer.py
Normal 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
|
||||
@@ -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
|
||||
219
lib/ai_marketing_tools/ai_google_ads_generator/ad_templates.py
Normal file
219
lib/ai_marketing_tools/ai_google_ads_generator/ad_templates.py
Normal 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
@@ -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>
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
1271
lib/ai_writers/ai_letter_writer/business_letters.py
Normal file
1271
lib/ai_writers/ai_letter_writer/business_letters.py
Normal file
File diff suppressed because it is too large
Load Diff
1135
lib/ai_writers/ai_letter_writer/cover_letters.py
Normal file
1135
lib/ai_writers/ai_letter_writer/cover_letters.py
Normal file
File diff suppressed because it is too large
Load Diff
1184
lib/ai_writers/ai_letter_writer/formal_letters.py
Normal file
1184
lib/ai_writers/ai_letter_writer/formal_letters.py
Normal file
File diff suppressed because it is too large
Load Diff
758
lib/ai_writers/ai_letter_writer/letter_templates.py
Normal file
758
lib/ai_writers/ai_letter_writer/letter_templates.py
Normal 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']}")
|
||||
236
lib/ai_writers/ai_letter_writer/main.py
Normal file
236
lib/ai_writers/ai_letter_writer/main.py
Normal 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()
|
||||
1121
lib/ai_writers/ai_letter_writer/personal_letter.py
Normal file
1121
lib/ai_writers/ai_letter_writer/personal_letter.py
Normal file
File diff suppressed because it is too large
Load Diff
493
lib/ai_writers/ai_letter_writer/utils/letter_analyzer.py
Normal file
493
lib/ai_writers/ai_letter_writer/utils/letter_analyzer.py
Normal 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}")
|
||||
545
lib/ai_writers/ai_letter_writer/utils/letter_formatter.py
Normal file
545
lib/ai_writers/ai_letter_writer/utils/letter_formatter.py
Normal 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"))
|
||||
988
lib/ai_writers/ai_letter_writer/utils/letter_templates.py
Normal file
988
lib/ai_writers/ai_letter_writer/utils/letter_templates.py
Normal 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']}")
|
||||
557
lib/ai_writers/ai_outline_writer/README.md
Normal file
557
lib/ai_writers/ai_outline_writer/README.md
Normal 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
|
||||
336
lib/ai_writers/ai_outline_writer/get_blog_outline.py
Normal file
336
lib/ai_writers/ai_outline_writer/get_blog_outline.py
Normal 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"\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
|
||||
489
lib/ai_writers/ai_outline_writer/outline_ui.py
Normal file
489
lib/ai_writers/ai_outline_writer/outline_ui.py
Normal 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()
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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. It’s 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.")
|
||||
@@ -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}")
|
||||
@@ -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}")
|
||||
208
lib/integrations/wix/README.md
Normal file
208
lib/integrations/wix/README.md
Normal 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
|
||||
850
lib/integrations/wix/wix_api_client.py
Normal file
850
lib/integrations/wix/wix_api_client.py
Normal 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}"
|
||||
720
lib/integrations/wix/wix_blog_manager.py
Normal file
720
lib/integrations/wix/wix_blog_manager.py
Normal 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()
|
||||
350
lib/integrations/wix/wix_blog_publisher.py
Normal file
350
lib/integrations/wix/wix_blog_publisher.py
Normal 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()
|
||||
388
lib/integrations/wix/wix_integration.py
Normal file
388
lib/integrations/wix/wix_integration.py
Normal 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)
|
||||
}
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user