Blog writer enhancements & fixes

This commit is contained in:
ajaysi
2025-04-29 08:55:47 +05:30
parent ef462f05f2
commit 9db20db0d1
45 changed files with 3000 additions and 3290 deletions

7
.gitignore vendored
View File

@@ -10,6 +10,13 @@
pycache
__pycache__
*.pyc
gemini-native-image*
*.mp4
*.mp3
*.wav
*.avi
*.mov
*.flv
*.pyo
*.pyd
*.pyw

View File

@@ -0,0 +1,635 @@
import os
import streamlit as st
from loguru import logger
from lib.utils.voice_processing import record_voice
from lib.ai_writers.ai_blog_writer.blog_writer_styles import apply_blog_writer_styles
from lib.ai_writers.ai_blog_writer.ai_blog_generator_utils import (
CONFIG_PATH,
load_config,
get_search_params_from_config,
get_blog_characteristics_from_config,
get_blog_images_from_config,
get_llm_options_from_config,
process_input,
handle_content_generation
)
apply_blog_writer_styles()
def display_input_section():
"""Display the input section with text area, file upload, and voice recording options."""
# Main container with columns for better organization
col1, col2, col3 = st.columns([2, 1.5, 0.5])
# First column: Keywords input
with col1:
st.markdown("### 📌 Content Source")
st.markdown("#### Enter Keywords, Title or URL")
user_input = st.text_area(
'Power your content with keywords or a website URL',
help='Provide keywords, a blog title, YouTube link, or web URL to generate targeted content.',
placeholder="Examples:\n- Keywords: AI tools, digital marketing\n- Blog Title: The Future of AI in Marketing\n- YouTube Link: https://youtube.com/...\n- Web URL: https://example.com/...",
height=150
)
# Second column: File uploader
with col2:
st.markdown("### 📁 File Upload")
st.markdown("#### Upload Reference Content")
uploaded_file = st.file_uploader(
"Add files to enhance your content",
type=["txt", "pdf", "docx", "jpg", "jpeg", "png", "mp3", "wav", "mp4", "mkv", "avi"],
help='Upload documents, images, or media files to incorporate additional information in your blog.'
)
# Third column: Voice input
with col3:
st.markdown("### 🎤 Voice")
st.markdown("#### Record Ideas")
audio_input = record_voice()
if audio_input:
st.success("Voice recorded!")
return user_input, uploaded_file, audio_input
def display_content_type_selection():
"""Display the content type selection section and return the selected type."""
# Content options in a cleaner layout
st.markdown("### 🔧 Content Configuration")
# Content type selection with better UI
st.markdown("#### Select Content Type")
content_type = st.radio(
"Choose the format and length of your blog content",
["Standard Blog Post", "Comprehensive Long-form", "AI Agent Team (Beta)"],
horizontal=True,
help="Standard: 800-1200 words | Long-form: 1500+ words | AI Agent: Experimental multi-perspective content"
)
# Map the friendly content type names to the original options
content_type_map = {
"Standard Blog Post": "Normal-length content",
"Comprehensive Long-form": "Long-form content",
"AI Agent Team (Beta)": "Experimental - AI Agents team"
}
return content_type, content_type_map[content_type]
def display_content_characteristics_tab():
"""Display the Content Characteristics tab and return the selected options."""
st.markdown("#### Blog Content Characteristics")
# Load default values from configuration
config_blog_chars = get_blog_characteristics_from_config()
# Blog length
blog_length = st.number_input(
"Blog Length (words)",
min_value=500,
max_value=5000,
value=int(config_blog_chars.get("blog_length", 2000)),
step=100,
help="Target word count for your blog post"
)
# Blog tone
tone_options = ["Professional", "Casual", "Formal", "Conversational", "Authoritative", "Friendly"]
default_tone = config_blog_chars.get("blog_tone", "Professional")
default_tone_index = tone_options.index(default_tone) if default_tone in tone_options else 0
blog_tone = st.selectbox(
"Blog Tone",
options=tone_options,
index=default_tone_index,
help="The overall tone and style of your blog content"
)
# Blog demographic
demographic_options = ["Professional", "General", "Technical", "Beginner", "Expert", "Student"]
default_demo = config_blog_chars.get("blog_demographic", "Professional")
default_demo_index = demographic_options.index(default_demo) if default_demo in demographic_options else 0
blog_demographic = st.selectbox(
"Target Audience",
options=demographic_options,
index=default_demo_index,
help="Who your blog content is primarily written for"
)
# Blog type
type_options = ["Informational", "How-to", "List", "Review", "Tutorial", "Opinion"]
default_type = config_blog_chars.get("blog_type", "Informational")
default_type_index = type_options.index(default_type) if default_type in type_options else 0
blog_type = st.selectbox(
"Blog Type",
options=type_options,
index=default_type_index,
help="The format and purpose of your blog content"
)
# Blog language
language_options = ["English", "Spanish", "French", "German", "Italian", "Portuguese"]
default_lang = config_blog_chars.get("blog_language", "English")
default_lang_index = language_options.index(default_lang) if default_lang in language_options else 0
blog_language = st.selectbox(
"Blog Language",
options=language_options,
index=default_lang_index,
help="The language your blog will be written in"
)
# Blog output format
format_options = ["markdown", "html", "plain text"]
default_format = config_blog_chars.get("blog_output_format", "markdown").lower()
default_format_index = format_options.index(default_format) if default_format in format_options else 0
blog_output_format = st.selectbox(
"Output Format",
options=format_options,
index=default_format_index,
help="The format in which the blog content will be generated"
)
# Show current configuration source
if os.path.exists(CONFIG_PATH):
st.success(f"✅ Using blog characteristics from configuration file")
else:
st.info(" Using default blog characteristics (no configuration file found)")
return {
"blog_length": blog_length,
"blog_tone": blog_tone,
"blog_demographic": blog_demographic,
"blog_type": blog_type,
"blog_language": blog_language,
"blog_output_format": blog_output_format
}
def display_content_analysis_tab():
"""Display the Content & Analysis Options tab and return the selected options."""
st.markdown("#### Content & Analysis Options")
# Create two columns for better organization
col1, col2 = st.columns(2)
with col1:
st.markdown("**Content Enhancements**")
create_seo_tags = st.checkbox(
'✅ Generate SEO metadata',
value=True,
help='Create schema markup, meta tags, and social media metadata'
)
generate_social_media = st.checkbox(
'✅ Create social media posts',
value=False,
help="Generate matching social content for Facebook, Twitter, and LinkedIn"
)
add_table_of_contents = st.checkbox(
'✅ Add table of contents',
value=True,
help="Include an auto-generated table of contents at the beginning of the blog"
)
with col2:
st.markdown("**Analysis & Improvement**")
content_analysis = st.checkbox(
'✅ Perform content analysis',
value=False,
help="Include proofreading, readability score, and improvement suggestions"
)
enhance_readability = st.checkbox(
'✅ Enhance readability',
value=True,
help="Optimize sentence structure and vocabulary for better readability"
)
fact_checking = st.checkbox(
'✅ Basic fact verification',
value=False,
help="Verify key facts from multiple sources when possible"
)
st.markdown("---")
st.markdown("**Formatting Options**")
# Create two columns for formatting options
fmt_col1, fmt_col2 = st.columns(2)
with fmt_col1:
section_headings = st.checkbox(
'✅ Use section headings',
value=True,
help="Include clear section headings throughout the blog"
)
include_lists = st.checkbox(
'✅ Use bullet points and lists',
value=True,
help="Format appropriate content as bullet points or numbered lists"
)
with fmt_col2:
include_quotes = st.checkbox(
'✅ Include relevant quotes',
value=False,
help="Add expert quotes or important statements as blockquotes"
)
use_subheadings = st.checkbox(
'✅ Use subheadings',
value=True,
help="Break down sections with descriptive subheadings"
)
return {
"create_seo_tags": create_seo_tags,
"generate_social_media": generate_social_media,
"add_table_of_contents": add_table_of_contents,
"content_analysis": content_analysis,
"enhance_readability": enhance_readability,
"fact_checking": fact_checking,
"section_headings": section_headings,
"include_lists": include_lists,
"include_quotes": include_quotes,
"use_subheadings": use_subheadings
}
def display_blog_images_tab():
"""Display the Blog Images Details tab and return the selected options."""
st.markdown("#### Blog Images Settings")
# Load default values from configuration
config_images = get_blog_images_from_config()
# Image generation model selection
model_options = ["stable-diffusion", "dall-e", "midjourney", "imagen"]
default_model = config_images.get("image_model", "stable-diffusion")
default_model_index = model_options.index(default_model) if default_model in model_options else 0
image_model = st.selectbox(
"Image Generation Model",
options=model_options,
index=default_model_index,
help="AI model used to generate blog images"
)
# Number of blog images
num_images = st.number_input(
"Number of Blog Images",
min_value=0,
max_value=10,
value=config_images.get("num_images", 1),
step=1,
help="Number of images to generate for the blog"
)
# Image style
style_options = ["Realistic", "Artistic", "Cartoon", "Minimalist", "Corporate", "Vibrant"]
default_style = config_images.get("image_style", "Realistic")
default_style_index = style_options.index(default_style) if default_style in style_options else 0
image_style = st.selectbox(
"Image Style",
options=style_options,
index=default_style_index,
help="Visual style of the generated images"
)
# Additional image options
st.markdown("**Additional Image Options**")
col1, col2 = st.columns(2)
with col1:
generate_featured = st.checkbox(
'✅ Generate featured image',
value=True,
help="Create a featured header image for the blog"
)
add_captions = st.checkbox(
'✅ Add image captions',
value=True,
help="Generate descriptive captions for each image"
)
with col2:
use_alt_text = st.checkbox(
'✅ Generate alt text',
value=True,
help="Create accessibility alt text for all images"
)
optimize_images = st.checkbox(
'✅ Optimize image placement',
value=True,
help="Intelligently place images throughout the content"
)
# Show current configuration source
if os.path.exists(CONFIG_PATH):
st.success(f"✅ Using image settings from configuration file")
else:
st.info(" Using default image settings (no configuration file found)")
return {
"image_model": image_model,
"num_images": num_images,
"image_style": image_style,
"generate_featured": generate_featured,
"add_captions": add_captions,
"use_alt_text": use_alt_text,
"optimize_placement": optimize_images
}
def display_llm_options_tab():
"""Display the LLM Options tab and return the selected options."""
st.markdown("#### Language Model Settings")
# Load default values from configuration
config_llm = get_llm_options_from_config()
# LLM provider selection
provider_options = ["google", "openai", "anthropic", "local"]
default_provider = config_llm.get("provider", "google")
default_provider_index = provider_options.index(default_provider) if default_provider in provider_options else 0
llm_provider = st.selectbox(
"AI Provider",
options=provider_options,
index=default_provider_index,
help="The AI provider to use for content generation"
)
# Model selection (dynamic based on provider)
if llm_provider == "google":
model_options = ["gemini-1.5-flash-latest", "gemini-1.5-pro-latest", "gemini-pro"]
elif llm_provider == "openai":
model_options = ["gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"]
elif llm_provider == "anthropic":
model_options = ["claude-3-opus", "claude-3-sonnet", "claude-3-haiku"]
else:
model_options = ["llama-3-70b", "mistral-large", "local-model"]
default_model = config_llm.get("model", "gemini-1.5-flash-latest")
default_model_index = 0
if default_model in model_options:
default_model_index = model_options.index(default_model)
llm_model = st.selectbox(
"AI Model",
options=model_options,
index=default_model_index,
help="The specific AI model to use for content generation"
)
# Create two columns for temperature and max tokens
col1, col2 = st.columns(2)
with col1:
# Temperature setting
temperature = st.slider(
"Temperature",
min_value=0.0,
max_value=1.0,
value=config_llm.get("temperature", 0.7),
step=0.1,
help="Controls randomness: lower values are more deterministic, higher values more creative"
)
with col2:
# Max tokens
max_tokens = st.number_input(
"Max Tokens",
min_value=1000,
max_value=32000,
value=config_llm.get("max_tokens", 4000),
step=1000,
help="Maximum length of generated content (in tokens)"
)
# Advanced LLM options
st.markdown("---")
st.markdown("**Advanced LLM Options**")
show_advanced_llm = st.checkbox("Show advanced LLM parameters", value=False)
advanced_params = {}
if show_advanced_llm:
# Top-p (nucleus sampling)
top_p = st.slider(
"Top-p (Nucleus Sampling)",
min_value=0.1,
max_value=1.0,
value=0.9,
step=0.1,
help="Controls diversity via nucleus sampling: 1.0 considers all tokens, lower values restrict to more likely tokens"
)
# Top-k
top_k = st.slider(
"Top-k",
min_value=1,
max_value=100,
value=40,
step=1,
help="Controls diversity by limiting to top k tokens: higher values allow more diversity"
)
# Presence penalty
presence_penalty = st.slider(
"Presence Penalty",
min_value=-2.0,
max_value=2.0,
value=0.0,
step=0.1,
help="Penalizes repeated tokens: positive values discourage repetition"
)
advanced_params = {
"top_p": top_p,
"top_k": top_k,
"presence_penalty": presence_penalty
}
# Show current configuration source
if os.path.exists(CONFIG_PATH):
st.success(f"✅ Using LLM settings from configuration file")
else:
st.info(" Using default LLM settings (no configuration file found)")
return {
"provider": llm_provider,
"model": llm_model,
"temperature": temperature,
"max_tokens": max_tokens,
**advanced_params
}
def display_search_settings_tab():
"""Display the Search Settings tab and return the selected options."""
st.markdown("#### AI Search Configuration")
st.markdown("Control how the AI researches your topic")
# Load default values from configuration
config_search_params = get_search_params_from_config()
# Number of search results
max_results = st.slider(
"Maximum Results",
min_value=5,
max_value=30,
value=config_search_params.get("max_results", 10),
step=5,
help="Maximum number of search results to use for research"
)
# Search depth
search_depth = st.radio(
"Search Depth",
options=["basic", "advanced"],
index=0,
horizontal=True,
help="Basic: Faster but less comprehensive. Advanced: More thorough but slower."
)
# Include domains
include_domains = st.text_input(
"Include Domains (Optional)",
value="",
help="Comma-separated list of domains to prioritize in search (e.g., wikipedia.org,nih.gov)"
)
# Time range - use value from config
time_options = ["day", "week", "month", "year", "all"]
default_time_index = time_options.index(config_search_params.get("time_range", "year")) if config_search_params.get("time_range", "year") in time_options else 3 # Default to "year" (index 3)
time_range = st.select_slider(
"Time Range",
options=time_options,
value=time_options[default_time_index],
help="Limit search results to a specific time period"
)
# Show current configuration source
if os.path.exists(CONFIG_PATH):
st.success(f"✅ Using search defaults from configuration file")
else:
st.info(" Using default search settings (no configuration file found)")
# Replace expander with checkbox for configuration display
show_config = st.checkbox("Show configuration details", value=False)
if show_config:
st.markdown("""
**Configuration File Location**
Search parameters are loaded from the main configuration file at:
`lib/workspace/alwrity_config/main_config.json`
You can modify this file to change the default search settings.
""")
if os.path.exists(CONFIG_PATH):
try:
with open(CONFIG_PATH, 'r') as f:
config_content = f.read()
st.code(config_content, language="json")
except:
st.warning("Could not read configuration file")
st.info("These settings control how the AI performs web research for your content. More thorough searches may take longer but produce better results.")
# Process include_domains from string to list if provided
domains_list = []
if include_domains:
domains_list = [domain.strip() for domain in include_domains.split(",") if domain.strip()]
return {
"max_results": max_results,
"search_depth": search_depth,
"time_range": time_range,
"include_domains": domains_list
}
def display_advanced_options():
"""Display all advanced options tabs and return the selected configurations."""
with st.expander("⚙️ Advanced Options", expanded=False):
tabs = st.tabs(["Content Characteristics", "Content & Analysis Options", "Blog Images Details", "LLM Options", "Search Settings"])
with tabs[0]: # Content Characteristics
blog_params = display_content_characteristics_tab()
with tabs[1]: # Combined Content & Analysis Options
content_analysis_params = display_content_analysis_tab()
with tabs[2]: # Blog Images Details
image_params = display_blog_images_tab()
with tabs[3]: # LLM Options
llm_params = display_llm_options_tab()
with tabs[4]: # Search Settings
search_params = display_search_settings_tab()
return blog_params, content_analysis_params, image_params, llm_params, search_params
def blog_from_keyword():
"""Input blog keywords, research and write a factual blog with enhanced UI."""
# Get user inputs
user_input, uploaded_file, audio_input = display_input_section()
# Get content type selection
content_type, selected_content_type = display_content_type_selection()
# Display advanced options and get configurations
blog_params, content_analysis_params, image_params, llm_params, search_params = display_advanced_options()
# Generate button with icon and clearer purpose
st.markdown("") # Add spacing
generate_pressed = st.button("✨ Generate Professional Blog Content", use_container_width=True)
# Processing logic
if generate_pressed:
st.empty()
if not uploaded_file and not user_input and not audio_input:
st.error("Please provide at least one input source (keywords, file, or voice recording)")
st.stop()
input_type = process_input(user_input, uploaded_file)
# Use the utility function to handle content generation
handle_content_generation(input_type, user_input, uploaded_file, search_params, blog_params, selected_content_type)
def ai_blog_writer_page():
"""Render the AI Blog Writer page with enhanced styling."""
logger.info("Rendering AI Blog Writer page")
# Apply shared blog writer styles
apply_blog_writer_styles()
# Back button with icon
if st.button("← Back to Dashboard", key="back_to_dashboard"):
logger.info("User clicked back button, returning to ai writer dashboard")
st.query_params.clear()
st.rerun()
# Enhanced header with icon
st.markdown("""
<div class="page-header">
<h1>✍️ AI Blog Writer</h1>
<p>Create engaging, SEO-optimized blog content with AI assistance. Our advanced algorithms help you generate high-quality, relevant articles for any topic or niche.</p>
</div>
""", unsafe_allow_html=True)
# Call the blog generator function with enhanced UI
logger.info("Calling blog_from_keyword function")
blog_from_keyword()
logger.info("Finished rendering AI Blog Writer page")

View File

@@ -0,0 +1,382 @@
import re
import os
import json
from loguru import logger
import PyPDF2
import streamlit as st
import tiktoken
import openai
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
from lib.ai_writers.keywords_to_blog_streamlit import write_blog_from_keywords
from lib.ai_writers.speech_to_blog.main_audio_to_blog import generate_audio_blog
from lib.ai_writers.long_form_ai_writer import long_form_generator
from lib.ai_writers.web_url_ai_writer import blog_from_url
from lib.ai_writers.image_ai_writer import blog_from_image
# Constants
CONFIG_PATH = os.path.join("lib", "workspace", "alwrity_config", "main_config.json")
DEFAULT_CONFIG = {
"Search Engine Parameters": {
"Geographic Location": "us",
"Search Language": "en",
"Number of Results": 10,
"Time Range": "year"
}
}
# Function to load configuration from JSON file
def load_config():
"""Load configuration from the main config JSON file."""
try:
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, 'r') as f:
config = json.load(f)
logger.info(f"Loaded configuration from {CONFIG_PATH}")
return config
else:
logger.warning(f"Configuration file not found at {CONFIG_PATH}, using defaults")
return DEFAULT_CONFIG
except Exception as e:
logger.error(f"Error loading configuration: {str(e)}")
return DEFAULT_CONFIG
# Function to get search parameters from config
def get_search_params_from_config():
"""Extract search parameters from the main configuration."""
config = load_config()
search_params = config.get("Search Engine Parameters", {})
# Map config values to expected parameter names
result = {
"max_results": search_params.get("Number of Results", 10),
"time_range": search_params.get("Time Range", "year").lower(),
"geo": search_params.get("Geographic Location", "us"),
"language": search_params.get("Search Language", "en")
}
# Normalize time_range to match our options
time_map = {
"day": "day",
"week": "week",
"month": "month",
"year": "year",
"anytime": "all",
"all": "all"
}
result["time_range"] = time_map.get(result["time_range"].lower(), "year")
logger.info(f"Using search parameters from config: {result}")
return result
# Function to get blog content characteristics from config
def get_blog_characteristics_from_config():
"""Extract blog content characteristics from the main configuration."""
config = load_config()
blog_characteristics = config.get("Blog Content Characteristics", {})
# Map config values to expected parameter names
result = {
"blog_length": blog_characteristics.get("Blog Length", "2000"),
"blog_tone": blog_characteristics.get("Blog Tone", "Professional"),
"blog_demographic": blog_characteristics.get("Blog Demographic", "Professional"),
"blog_type": blog_characteristics.get("Blog Type", "Informational"),
"blog_language": blog_characteristics.get("Blog Language", "English"),
"blog_output_format": blog_characteristics.get("Blog Output Format", "markdown")
}
logger.info(f"Using blog characteristics from config: {result}")
return result
# Function to get blog image details from config
def get_blog_images_from_config():
"""Extract blog image details from the main configuration."""
config = load_config()
blog_images = config.get("Blog Images Details", {})
# Map config values to expected parameter names
result = {
"image_model": blog_images.get("Image Generation Model", "stable-diffusion"),
"num_images": int(blog_images.get("Number of Blog Images", 1)),
"image_style": blog_images.get("Image Style", "Realistic")
}
logger.info(f"Using blog image details from config: {result}")
return result
# Function to get LLM options from config
def get_llm_options_from_config():
"""Extract LLM options from the main configuration."""
config = load_config()
llm_options = config.get("LLM Options", {})
# Map config values to expected parameter names
result = {
"provider": llm_options.get("GPT Provider", "google"),
"model": llm_options.get("Model", "gemini-1.5-flash-latest"),
"temperature": float(llm_options.get("Temperature", 0.7)),
"max_tokens": int(llm_options.get("Max Tokens", 4000))
}
logger.info(f"Using LLM options from config: {result}")
return result
# Split a text into smaller chunks of size n, preferably ending at the end of a sentence
def create_chunks(text, n, tokenizer):
tokens = tokenizer.encode(text)
"""Yield successive n-sized chunks from text."""
i = 0
while i < len(tokens):
# Find the nearest end of sentence within a range of 0.5 * n and 1.5 * n tokens
j = min(i + int(1.5 * n), len(tokens))
while j > i + int(0.5 * n):
# Decode the tokens and check for full stop or newline
chunk = tokenizer.decode(tokens[i:j])
if chunk.endswith(".") or chunk.endswith("\n"):
break
j -= 1
# If no end of sentence found, use n tokens as the chunk size
if j == i + int(0.5 * n):
j = min(i + n, len(tokens))
yield tokens[i:j]
i = j
def extract_chunk(document, template_prompt):
""" Chunking for large documents, exceed context window"""
prompt = template_prompt.replace('<document>', document)
try:
response = llm_text_gen(prompt)
return response
except Exception as err:
logger.error(f"Failed to get response from LLM: {err}")
raise
def blog_from_pdf(pdf_text):
"""
Load in a long PDF and extract key information.
Chunk up document and process each chunk, then combine them.
"""
template_prompt=f'''Extract key pieces of information from the given document.
When you extract a key piece of information, include the closest page number.
Ex: Extracted Information (Page number)
\n\nDocument: \"\"\"<document>\"\"\"\n\n'''
# Initialize tokenizer
tokenizer = tiktoken.get_encoding("cl100k_base")
results = []
chunks = create_chunks(pdf_text, 1000, tokenizer)
text_chunks = [tokenizer.decode(chunk) for chunk in chunks]
for chunk in text_chunks:
try:
results.append(extract_chunk(chunk, template_prompt))
except Exception as e:
logger.error(f"Error processing chunk: {e}")
# Continue with other chunks even if one fails
continue
return results
# Input validation functions
def is_youtube_link(text):
"""Check if text is a valid YouTube link."""
if text is not None:
youtube_regex = re.compile(r'(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})')
return youtube_regex.match(text)
return False
def is_web_link(text):
"""Check if text is a valid web link."""
if text is not None:
web_regex = re.compile(r'(https?://)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)')
return web_regex.match(text)
return False
def process_input(input_text, uploaded_file):
"""
Determine the type of input provided by the user.
Args:
input_text (str): The text input from the user
uploaded_file: The file uploaded by the user
Returns:
str: The determined input type ("youtube_url", "web_url", "keywords", "PDF_file", "image_file", "audio_file", "video_file", or None)
"""
# Process text input
if input_text:
if is_youtube_link(input_text):
if input_text.startswith("https://www.youtube.com/") or input_text.startswith("http://www.youtube.com/"):
return "youtube_url"
else:
st.error("Invalid YouTube URL. Please enter a valid URL.")
return None
elif is_web_link(input_text):
return "web_url"
else:
return "keywords"
# Process file input
if uploaded_file is not None:
file_details = {"filename": uploaded_file.name, "filetype": uploaded_file.type}
st.write(file_details)
# Handle different file types
if uploaded_file.type.startswith("text/"):
content = uploaded_file.read().decode("utf-8")
st.text(content)
return "text_file"
elif uploaded_file.type == "application/pdf":
return "PDF_file"
elif uploaded_file.type in ["application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/msword"]:
st.write("Word document uploaded. Add your DOCX processing logic here.")
return "word_file"
elif uploaded_file.type.startswith("image/"):
st.image(uploaded_file)
return "image_file"
elif uploaded_file.type.startswith("audio/"):
st.audio(uploaded_file)
return "audio_file"
elif uploaded_file.type.startswith("video/"):
st.video(uploaded_file)
return "video_file"
return None
# Content processing functions
def process_keywords_input(user_input, search_params, blog_params, selected_content_type):
"""Process keywords input and generate content based on the selected options."""
if not user_input or len(user_input.split()) < 2:
st.error('Please provide at least two keywords for best results')
return False
try:
if selected_content_type == "Normal-length content":
st.subheader("Your Generated Blog Post")
logger.info(f"Generating standard blog post with parameters: {blog_params}")
# Ensure all blog parameters are properly passed
# This is important as the UI may have settings that aren't in the default blog_params
short_blog = write_blog_from_keywords(
user_input,
search_params=search_params,
blog_params=blog_params
)
st.markdown(short_blog)
return True
elif selected_content_type == "Long-form content":
logger.info(f"Generating long-form content with parameters: {blog_params}")
# Ensure all blog parameters are properly passed to long-form generator
long_form_generator(
user_input,
search_params=search_params,
blog_params=blog_params
)
st.success(f"Successfully generated long-form content for: {user_input}")
return True
else:
st.info("AI Agent Team feature is coming soon! This will provide multi-perspective content with different AI experts collaborating on your blog.")
return False
except Exception as err:
logger.error(f"An error occurred while generating content: {err}")
st.error(f"An error occurred while generating content: {err}")
return False
def process_pdf_input(uploaded_file):
"""Process a PDF file and generate content."""
with st.expander("Processing PDF Document", expanded=True):
pdf_reader = PyPDF2.PdfReader(uploaded_file)
text = ""
combined_result = ""
# Show progress with better UI
progress_text = st.empty()
progress_bar = st.progress(0)
total_pages = len(pdf_reader.pages)
for page_num, page in enumerate(pdf_reader.pages):
progress_text.text(f"Processing page {page_num+1}/{total_pages}")
text += page.extract_text()
text = text.replace("\n", " ")
text = re.sub(r"(\w)([A-Z])", r"\1 \2", text)
results = blog_from_pdf(text)
progress_percent = (page_num + 1) / total_pages
progress_bar.progress(progress_percent)
combined_result += str(results[-1])
progress_text.empty()
progress_bar.empty()
st.subheader("Generated Content from PDF")
st.markdown(combined_result)
return True
def process_youtube_or_audio(user_input):
"""Process a YouTube URL or audio file and generate content."""
if not generate_audio_blog(user_input):
return False
return True
def process_web_url(user_input):
"""Process a web URL and generate content."""
blog_from_url(user_input)
return True
def process_image_input(user_input, uploaded_file):
"""Process an image file and generate content."""
blog_from_image(user_input, uploaded_file)
return True
def handle_content_generation(input_type, user_input, uploaded_file, search_params, blog_params, selected_content_type):
"""
Handle content generation based on the input type.
Args:
input_type: The type of input ("youtube_url", "web_url", etc.)
user_input: The text input from the user
uploaded_file: The uploaded file (if any)
search_params: Search parameters
blog_params: Blog content parameters
selected_content_type: The selected content type
Returns:
bool: True if content generation was successful, False otherwise
"""
with st.spinner("Crafting your blog content... Please wait."):
if input_type == "keywords":
return process_keywords_input(user_input, search_params, blog_params, selected_content_type)
elif input_type == "youtube_url" or input_type == "audio_file":
return process_youtube_or_audio(user_input)
elif input_type == "web_url":
return process_web_url(user_input)
elif input_type == "image_file":
return process_image_input(user_input, uploaded_file)
elif input_type == "PDF_file":
return process_pdf_input(uploaded_file)
else:
st.error(f"Unsupported input type: {input_type}")
return False

View File

@@ -0,0 +1,252 @@
import streamlit as st
def apply_blog_writer_styles():
st.markdown("""
<style>
/* Base UI improvements */
body, .main .block-container {
background: linear-gradient(135deg, #f0f4f8 0%, #d7e1ec 100%) !important;
min-height: 100vh;
color: #2c3e50;
font-family: 'Helvetica Neue', sans-serif;
}
/* Main layout improvements */
.main .block-container {
padding: 1rem 2rem 2rem 2rem !important;
max-width: 1200px;
margin: 0 auto;
}
/* Back button styling */
[data-testid="stButton"] > button:first-of-type {
background: #1976d2;
color: white;
border: none;
border-radius: 30px;
padding: 0.5rem 1.5rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
box-shadow: 0 2px 10px rgba(25, 118, 210, 0.2);
margin-bottom: 1.5rem;
}
[data-testid="stButton"] > button:first-of-type:hover {
background: #1565c0;
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(25, 118, 210, 0.3);
}
/* Header styling */
.blog-header, .page-header {
text-align: center;
margin-bottom: 2rem;
padding: 2rem 1.5rem;
background: linear-gradient(135deg, #ffffff 0%, #f5f7fa 100%);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.blog-header h1, .page-header h1 {
font-size: 2.5em;
font-family: 'Helvetica Neue', sans-serif;
font-weight: 700;
color: #1976d2;
margin-bottom: 0.5rem;
}
.blog-header p, .page-header p {
font-size: 1.1em;
color: #546e7a;
max-width: 700px;
margin: 0 auto;
line-height: 1.6;
}
/* Input section styling */
.stTextArea textarea, .stTextInput input {
background: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 0.75rem 1rem;
color: #2c3e50;
font-size: 1rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: all 0.2s ease;
}
.stTextArea textarea:focus, .stTextInput input:focus {
border: 1.5px solid #1976d2;
box-shadow: 0 2px 10px rgba(25, 118, 210, 0.12);
}
/* File uploader styling */
.stFileUploader > div {
background: #ffffff;
border: 2px dashed #cfd8dc;
border-radius: 10px;
padding: 1.5rem 1rem;
text-align: center;
transition: all 0.2s ease;
}
.stFileUploader > div:hover {
border-color: #1976d2;
background: rgba(25, 118, 210, 0.03);
}
/* Options expander styling */
.stExpander {
border-radius: 10px;
overflow: hidden;
margin: 1.5rem 0;
border: 1px solid #e0e0e0;
}
.stExpander > details {
background: #ffffff;
padding: 0.5rem;
}
.stExpander > details > summary {
padding: 0.75rem 1rem;
font-weight: 600;
color: #1976d2;
}
.stExpander > details > summary:hover {
color: #1565c0;
}
/* Checkbox styling */
.stCheckbox > div {
background: #ffffff;
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 0.5rem;
border: 1px solid #f0f0f0;
}
.stCheckbox > div:hover {
background: rgba(25, 118, 210, 0.03);
}
.stCheckbox label {
font-weight: 500;
color: #455a64;
}
/* Radio button styling */
.stRadio > div {
background: #ffffff;
border-radius: 10px;
padding: 0.5rem;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
border: 1px solid #f0f0f0;
}
.stRadio > div > div {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.stRadio > div > div > label {
background: #f5f7fa;
padding: 0.6rem 1.2rem;
border-radius: 30px;
transition: all 0.2s ease;
text-align: center;
font-weight: 500;
color: #546e7a;
border: 1px solid #e0e0e0;
}
.stRadio > div > div > label:hover {
background: #e3f2fd;
color: #1976d2;
border-color: #bbdefb;
}
.stRadio > div > div > label[data-baseweb="radio"] input:checked + div {
background: #1976d2;
color: white;
border-color: #1976d2;
}
/* Generate button styling */
button[data-testid="baseButton-secondary"],
button[data-testid="baseButton-primary"] {
background: linear-gradient(45deg, #1976d2, #2196f3);
color: white;
border: none;
border-radius: 30px;
padding: 0.85rem 1.5rem;
font-weight: 600;
font-size: 1.1rem;
letter-spacing: 0.5px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(25, 118, 210, 0.25);
text-transform: uppercase;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
button[data-testid="baseButton-secondary"]:hover,
button[data-testid="baseButton-primary"]:hover {
background: linear-gradient(45deg, #1565c0, #1976d2);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(25, 118, 210, 0.35);
}
/* Input labels */
.stTextArea label, .stTextInput label, .stFileUploader label {
font-weight: 600;
color: #455a64;
font-size: 1.05rem;
margin-bottom: 0.5rem;
}
/* Section headers */
.stMarkdown h3 {
color: #1976d2;
font-weight: 600;
font-size: 1.3rem;
margin: 1.5rem 0 0.75rem 0;
padding-bottom: 0.5rem;
border-bottom: 2px solid rgba(25, 118, 210, 0.1);
}
.stMarkdown h4 {
color: #455a64;
font-weight: 600;
font-size: 1.1rem;
margin: 1rem 0 0.5rem 0;
}
/* Column layout improvements */
[data-testid="column"] {
background: #ffffff;
border-radius: 12px;
padding: 1.2rem;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.04);
border: 1px solid rgba(0, 0, 0, 0.05);
margin: 0 0.5rem;
}
/* Success and error messages */
.stSuccess, .stInfo, .stError {
border-radius: 10px;
padding: 1rem;
margin: 1rem 0;
font-weight: 500;
}
</style>
""", unsafe_allow_html=True)

View File

@@ -0,0 +1,273 @@
import streamlit as st
from lib.utils.alwrity_utils import (essay_writer, ai_news_writer, ai_finance_ta_writer)
from lib.ai_writers.ai_story_writer.story_writer import story_input_section
from lib.ai_writers.ai_product_description_writer import write_ai_prod_desc
from lib.ai_writers.ai_copywriter.copywriter_dashboard import copywriter_dashboard
from lib.ai_writers.linkedin_writer import LinkedInAIWriter
#from lib.content_planning_calender.content_planning_agents_alwrity_crew import ai_agents_content_planner
from lib.ai_writers.ai_blog_writer.ai_blog_generator import ai_blog_writer_page
from loguru import logger
def list_ai_writers():
"""Return a list of available AI writers with their metadata (no UI rendering)."""
return [
{
"name": "AI Blog Writer",
"icon": "📝",
"description": "Generate comprehensive blog posts from keywords, URLs, or uploaded content",
"category": "Content Creation",
"function": ai_blog_writer_page,
"path": "ai_blog_writer"
},
{
"name": "Story Writer",
"icon": "📚",
"description": "Create engaging stories and narratives with AI assistance",
"category": "Creative Writing",
"function": story_input_section,
"path": "story_writer"
},
{
"name": "Essay writer",
"icon": "✍️",
"description": "Generate well-structured essays on any topic",
"category": "Academic",
"function": essay_writer,
"path": "essay_writer"
},
{
"name": "Write News reports",
"icon": "📰",
"description": "Create professional news articles and reports",
"category": "Journalism",
"function": ai_news_writer,
"path": "news_writer"
},
{
"name": "Write Financial TA report",
"icon": "📊",
"description": "Generate technical analysis reports for financial markets",
"category": "Finance",
"function": ai_finance_ta_writer,
"path": "financial_writer"
},
{
"name": "AI Product Description Writer",
"icon": "🛍️",
"description": "Create compelling product descriptions that drive sales",
"category": "E-commerce",
"function": write_ai_prod_desc,
"path": "product_writer"
},
{
"name": "AI Copywriter",
"icon": "✒️",
"description": "Generate persuasive copy for marketing and advertising",
"category": "Marketing",
"function": copywriter_dashboard,
"path": "copywriter"
},
{
"name": "LinkedIn AI Writer",
"icon": "💼",
"description": "Create professional LinkedIn content that engages your network",
"category": "Professional",
"function": lambda: LinkedInAIWriter().run(),
"path": "linkedin_writer"
}
]
def get_ai_writers():
"""Render the AI Writers dashboard UI with a professional, clickable card layout."""
logger.info("Initializing AI Writers Dashboard")
writers = list_ai_writers()
logger.info(f"Found {len(writers)} AI writers")
# Add custom CSS for a professional dashboard with VIBRANT clickable cards
st.markdown("""
<style>
/* Base UI improvements */
body, .main .block-container {
background: linear-gradient(135deg, #f0f4f8 0%, #e6eef7 100%) !important;
min-height: 100vh;
color: #2c3e50;
font-family: 'Helvetica Neue', sans-serif;
}
/* Main layout improvements */
.main .block-container {
padding: 1.5rem 2rem 2rem 2rem !important;
max-width: 1200px;
margin: 0 auto;
}
/* Dashboard header */
.dashboard-header {
text-align: center;
margin-bottom: 2.5rem;
padding: 2rem 1.5rem;
background: linear-gradient(135deg, #ffffff 0%, #f5f7fa 100%);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.07);
border: 1px solid rgba(0, 0, 0, 0.04);
}
.dashboard-header h1 {
font-size: 2.6em;
font-family: 'Helvetica Neue', sans-serif;
font-weight: 700;
color: #1976d2;
margin-bottom: 0.5rem;
}
.dashboard-header p {
font-size: 1.15em;
color: #546e7a;
max-width: 700px;
margin: 0 auto;
line-height: 1.6;
}
/* Styling st.button to look like a clickable card - PREMIUM VIBRANT */
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button {
/* Vivid Gradient Background - More saturated blue-purple */
background: linear-gradient(135deg, #8a2be2 0%, #4169e1 100%);
color: #ffffff;
border: none;
padding: 1.8rem 1.5rem;
border-radius: 18px;
font-weight: 500;
font-size: 1rem;
font-family: 'Helvetica Neue', sans-serif;
transition: all 0.35s cubic-bezier(0.25, 0.8, 0.25, 1); /* Smoother, more premium transition */
box-shadow: 0 8px 20px rgba(77, 5, 232, 0.25), 0 2px 6px rgba(0, 0, 0, 0.15); /* Layered shadow for depth */
width: 100%;
height: 100%;
min-height: 190px;
margin-bottom: 0;
text-align: left;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
line-height: 1.5;
overflow: hidden;
transform: translateY(0); /* Starting position for hover animation */
position: relative; /* For pseudo-element effects */
}
/* Subtle shine effect on cards */
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button::after {
content: '';
position: absolute;
top: 0;
left: -50%;
width: 150%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
transform: skewX(-20deg);
transition: 0.5s;
opacity: 0;
}
/* Dynamic hover effects with gradient shift */
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button:hover {
background: linear-gradient(135deg, #4169e1 0%, #8a2be2 100%); /* Reverse gradient on hover */
transform: translateY(-8px) scale(1.05); /* More dramatic lift */
box-shadow: 0 15px 30px rgba(77, 5, 232, 0.4), 0 5px 15px rgba(0, 0, 0, 0.2); /* Deeper shadow */
color: #ffffff;
}
/* Animate shine on hover */
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button:hover::after {
left: 100%;
opacity: 1;
}
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button:hover > div > p strong {
color: #ffffff; /* Bright white on hover */
text-shadow: 0 0 15px rgba(255,255,255,0.5); /* Glow effect on hover */
}
/* Target the paragraph generated by Streamlit inside the button */
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button > div > p {
margin: 0;
line-height: 1.5;
color: rgba(255, 255, 255, 0.9); /* Slightly dimmed base text */
}
/* Icon (first line/element) - MORE PROMINENT */
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button > div > p::first-line {
font-size: 2.3em; /* Larger icon */
line-height: 1.2;
display: block;
margin-bottom: 1rem;
color: #ffffff;
text-shadow: 0 0 10px rgba(255,255,255,0.4); /* Light glow effect */
}
/* Title (strong tag from markdown) - ENHANCED CONTRAST */
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button > div > p strong {
font-size: 1.4em; /* Larger title */
font-weight: 700; /* Bolder */
color: #ffffff;
display: block;
margin: 0.7rem 0;
text-shadow: 1px 1px 4px rgba(0,0,0,0.3); /* Stronger shadow for better contrast */
letter-spacing: 0.5px; /* Slight letter spacing for premium feel */
}
/* Description - IMPROVED CONTRAST */
[data-testid="stVerticalBlock"] [data-testid="stButton"] > button > div > p {
font-size: 1rem; /* Slightly larger for readability */
color: rgba(255, 255, 255, 0.95); /* Better contrast */
text-shadow: 0 1px 2px rgba(0,0,0,0.1); /* Subtle shadow for text */
}
/* Column adjustments for consistent card height */
[data-testid="column"] {
display: flex;
flex-direction: column;
gap: 1.5rem; /* Consistent gap */
}
/* Hide Streamlit default title */
.stApp > header {
visibility: hidden;
}
</style>
""", unsafe_allow_html=True)
# Dashboard header
st.markdown("""
<div class="dashboard-header">
<h1>🚀 AI Content Creation Suite</h1>
<p>Welcome! Select the perfect AI writer tool from the options below to start creating amazing content.</p>
</div>
""", unsafe_allow_html=True)
# Create columns for the grid layout
cols = st.columns(3)
# Render buttons styled as cards for each writer
for idx, writer in enumerate(writers):
with cols[idx % 3]:
# Prepare the button label using simple Markdown with newlines
button_label = f"{writer['icon']}\n**{writer['name']}**\n{writer['description']}"
if st.button(
button_label,
key=f"writer_{writer['path']}",
help=f"Click to use the {writer['name']}", # More specific help text
use_container_width=True,
):
logger.info(f"Selected writer: {writer['name']} with path: {writer['path']}")
st.session_state.selected_writer = writer
st.query_params["writer"] = writer['path']
logger.info(f"Updated query params with writer: {writer['path']}")
st.rerun()
logger.info("Finished rendering AI Writers Dashboard")
# Return writers list, though it's not strictly needed if only rendering UI
return writers
# Remove the old ai_writers function since it's now integrated into get_ai_writers

View File

@@ -13,26 +13,114 @@ logger.add(sys.stdout,
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
def write_blog_google_serp(search_keyword, search_results):
"""Combine the given online research and GPT blog content"""
prompt = f"""
As expert Creative Content writer,
I want you to write highly detailed blog post, that explores {search_keyword} and also include 5 FAQs.
I want the post to offer unique insights, relatable examples, and a fresh perspective on the topic.
Here are some Google search results to spark your creativity on {search_keyword}:
\n\n
\"\"\"{search_results}\"\"\"
"""
def write_blog_google_serp(keywords, search_results, blog_params=None):
"""
Write a blog post using search results from Google SERP.
logger.info("Generating blog and FAQs from Google web search results.")
Args:
keywords (str): The keywords or topic for the blog
search_results (dict): Results from Google SERP search
blog_params (dict, optional): Blog content characteristics:
- blog_length: Target word count
- blog_tone: Content tone
- blog_demographic: Target audience
- blog_type: Type of blog post
- blog_language: Language for the blog
Returns:
str: The generated blog content in markdown format
"""
# If no blog parameters are provided, use defaults
if blog_params is None:
blog_params = {
"blog_length": 2000,
"blog_tone": "Professional",
"blog_demographic": "Professional",
"blog_type": "Informational",
"blog_language": "English"
}
# Ensure all parameters have default values
blog_length = blog_params.get("blog_length", 2000)
blog_tone = blog_params.get("blog_tone", "Professional")
blog_demographic = blog_params.get("blog_demographic", "Professional")
blog_type = blog_params.get("blog_type", "Informational")
blog_language = blog_params.get("blog_language", "English")
logger.info(f"Generating {blog_tone} {blog_type} blog of {blog_length} words for {blog_demographic} audience in {blog_language}")
try:
response = llm_text_gen(prompt)
# Build a prompt based on search results
prompt_parts = [
f"You are a specialized blog writer who writes in a {blog_tone} tone for a {blog_demographic} audience. "
f"Create a {blog_type} blog post that is approximately {blog_length} words in {blog_language}.",
f"The blog should be about: {keywords}",
"Use the following search results to create an informative, accurate, and well-structured blog post:"
]
# Add organic search results
if 'organic' in search_results:
prompt_parts.append("\nSearch results:")
for i, result in enumerate(search_results['organic'][:5], 1):
title = result.get('title', 'No title')
snippet = result.get('snippet', 'No snippet')
prompt_parts.append(f"{i}. {title}: {snippet}")
# Add people also ask questions if available
if 'peopleAlsoAsk' in search_results and search_results['peopleAlsoAsk']:
prompt_parts.append("\nPeople also ask:")
for i, question in enumerate(search_results['peopleAlsoAsk'][:3], 1):
q_text = question.get('question', 'No question')
q_answer = question.get('answer', {}).get('snippet', 'No answer')
prompt_parts.append(f"{i}. Q: {q_text}\n A: {q_answer}")
# Add related searches if available
if 'relatedSearches' in search_results and search_results['relatedSearches']:
related = [item.get('query', '') for item in search_results['relatedSearches'][:5]]
if related:
prompt_parts.append("\nRelated topics to consider including:")
prompt_parts.append(", ".join(related))
# Add specific instructions based on blog_type
type_instructions = {
"Informational": "Focus on providing factual information and educating the reader about the topic.",
"How-to": "Include clear step-by-step instructions with actionable advice.",
"List": "Organize content into a numbered or bulleted list of points, tips, or examples.",
"Review": "Provide balanced analysis with pros and cons, and a clear conclusion or recommendation.",
"Tutorial": "Include detailed instructions with examples and explanations for each step.",
"Opinion": "Present a clear perspective supported by evidence, while acknowledging other viewpoints."
}
prompt_parts.append(f"\nSpecific instructions: {type_instructions.get(blog_type, '')}")
# Add formatting instructions
prompt_parts.append("""
Format the blog post in markdown with:
- A compelling title (# Title)
- An introduction that hooks the reader
- Well-structured sections with appropriate headings (## Headings)
- Bullet points or numbered lists where appropriate
- A conclusion summarizing key points
- Make sure all content is accurate, informative, and adds value to the reader.
- Include 2-3 subheadings to organize the content well.
- Be concise and to the point.
- Write in an engaging, reader-friendly style.
- Avoid using phrases like "According to the search results" or "Based on the information provided."
- Present information as direct knowledge.
""")
# Combine all prompt parts
full_prompt = "\n".join(prompt_parts)
# Generate the blog content using the prompt
response = llm_text_gen(full_prompt)
# Return the generated content
return response
except Exception as err:
logger.error(f"Exit: Failed to get response from LLM: {err}")
exit(1)
logger.error(f"Error generating blog from search results: {err}")
raise
def improve_blog_intro(blog_content, blog_intro):

File diff suppressed because it is too large Load Diff

View File

@@ -53,216 +53,177 @@ def generate_with_retry(prompt, system_prompt=None):
return False
def long_form_generator(content_keywords):
def long_form_generator(keywords, search_params=None, blog_params=None):
"""
Write long form content using prompt chaining and iterative generation.
Generate a long-form blog post based on the given keywords
Parameters:
content_keywords (str): The main keywords or topic for the long-form content.
Args:
keywords (str): Topic or keywords for the blog post
search_params (dict, optional): Search parameters for research
blog_params (dict, optional): Blog content characteristics
"""
# Initialize default parameters if not provided
if blog_params is None:
blog_params = {
"blog_length": 3000, # Default longer for long-form content
"blog_tone": "Professional",
"blog_demographic": "Professional",
"blog_type": "Informational",
"blog_language": "English"
}
else:
# Ensure we have a higher word count for long-form content
if blog_params.get("blog_length", 0) < 2500:
blog_params["blog_length"] = max(3000, blog_params.get("blog_length", 0))
# Extract parameters with defaults
blog_length = blog_params.get("blog_length", 3000)
blog_tone = blog_params.get("blog_tone", "Professional")
blog_demographic = blog_params.get("blog_demographic", "Professional")
blog_type = blog_params.get("blog_type", "Informational")
blog_language = blog_params.get("blog_language", "English")
st.subheader(f"Long-form {blog_type} Blog ({blog_length}+ words)")
with st.status("Generating comprehensive long-form content...", expanded=True) as status:
# Step 1: Generate outline
status.update(label="Creating detailed content outline...")
# Use a customized prompt based on the blog parameters
outline_prompt = f"""
As an expert content strategist writing in a {blog_tone} tone for {blog_demographic} audience,
create a detailed outline for a comprehensive {blog_type} blog post about "{keywords}"
that will be approximately {blog_length} words in {blog_language}.
The outline should include:
1. An engaging headline
2. 5-7 main sections with descriptive headings
3. 2-3 subsections under each main section
4. Key points to cover in each section
5. Ideas for relevant examples or case studies
6. Suggestions for data points or statistics to include
Format the outline in markdown with proper headings and bullet points.
"""
try:
outline = llm_text_gen(outline_prompt)
st.markdown("### Content Outline")
st.markdown(outline)
status.update(label="Outline created successfully ✓")
# Step 2: Research the topic using the search parameters
status.update(label="Researching topic details...")
research_results = research_topic(keywords, search_params)
status.update(label="Research completed ✓")
# Step 3: Generate the full content
status.update(label=f"Writing {blog_length}+ word {blog_tone} {blog_type} content...")
full_content_prompt = f"""
You are a professional content writer who specializes in {blog_type} content with a {blog_tone} tone
for {blog_demographic} audiences. Write a comprehensive, in-depth blog post in {blog_language} about:
"{keywords}"
Use this outline as your structure:
{outline}
And incorporate these research findings where relevant:
{research_results}
The blog post should:
- Be approximately {blog_length} words
- Include an engaging introduction and strong conclusion
- Use appropriate subheadings for all sections in the outline
- Include examples, data points, and actionable insights
- Be formatted in markdown with proper headings, bullet points, and emphasis
- Maintain a {blog_tone} tone throughout
- Address the needs and interests of a {blog_demographic} audience
Do not include phrases like "according to research" or "based on the outline" in your content.
"""
full_content = llm_text_gen(full_content_prompt)
status.update(label="Long-form content generated successfully! ✓", state="complete")
# Display the full content
st.markdown("### Your Complete Long-form Blog Post")
st.markdown(full_content)
return full_content
except Exception as e:
status.update(label=f"Error generating long-form content: {str(e)}", state="error")
st.error(f"Failed to generate long-form content: {str(e)}")
return None
def research_topic(keywords, search_params=None):
"""
Research a topic using search parameters and return a summary
Args:
keywords (str): Topic to research
search_params (dict, optional): Search parameters
Returns:
str: The generated long-form content.
str: Research summary
"""
with st.status("Start Writing Long Form Article, Hold my Beer..", expanded=True) as status:
# Read the main_config to define tone, character, personality of the content to be generated.
try:
status.update(label=f"Starting to write content on {content_keywords}.")
logger.info(f"Starting to write content on {content_keywords}.")
# Define persona and writing guidelines
content_tone, target_audience, content_type, content_language, output_format, content_length = read_return_config_section('blog_characteristics')
except Exception as err:
logger.error(f"Failed to Read config params from main_config: {err}")
st.error(f"Failed to Read config params from main_config: {err}")
return False
# Display a placeholder for research results
placeholder = st.empty()
placeholder.info("Researching topic... Please wait.")
try:
filepath = os.path.join(os.environ["PROMPTS_DIR"], "long_form_ai_writer.prompts")
status.update(label=f"Reading Prompts from {filepath}.")
# Check if file exists
if not os.path.exists(filepath):
raise FileNotFoundError(f"File {filepath} does not exist")
with open(filepath, 'r') as file:
prompts = yaml.safe_load(file)
except Exception as err:
st.error(f"Exit: Failed to read prompts from {filepath}: {err}")
logger.error(f"Exit: Failed to read prompts from {filepath}: {err}")
exit(1)
writing_guidelines = prompts.get('writing_guidelines').format(
content_language=content_language,
content_tone=content_tone,
content_type=content_type,
output_format=output_format,
content_keywords=content_keywords,
target_audience=target_audience
)
content_title = prompts.get('content_title').format(
content_language=content_language,
content_keywords=content_keywords,
target_audience=target_audience
try:
from .keywords_to_blog_streamlit import do_tavily_ai_search
# Use provided search params or defaults
if search_params is None:
search_params = {
"max_results": 10,
"search_depth": "advanced",
"time_range": "year"
}
# Conduct research using Tavily
tavily_results = do_tavily_ai_search(
keywords,
max_results=search_params.get("max_results", 10),
search_depth=search_params.get("search_depth", "advanced"),
include_domains=search_params.get("include_domains", []),
time_range=search_params.get("time_range", "year")
)
content_outline = prompts.get('content_outline').format(
content_language=content_language,
content_title='{content_title}',
content_type=content_type,
target_audience=target_audience
)
# Extract research data
research_data = ""
if tavily_results and len(tavily_results) == 3:
results, titles, answer = tavily_results
if answer and len(answer) > 50:
research_data += f"Summary: {answer}\n\n"
if results and 'results' in results and len(results['results']) > 0:
research_data += "Key Sources:\n"
for i, result in enumerate(results['results'][:7], 1):
title = result.get('title', 'Untitled Source')
content_snippet = result.get('content', '')[:300] + "..."
research_data += f"{i}. {title}\n{content_snippet}\n\n"
starting_prompt = prompts.get('starting_prompt').format(
content_language=content_language,
content_title='{content_title}',
content_outline='{content_outline}',
writing_guidelines=writing_guidelines
)
# If research data is empty or too short, provide a generic response
if not research_data or len(research_data) < 100:
research_data = f"No specific research data found for '{keywords}'. Please provide more specific information in your content."
continuation_prompt = prompts.get('continuation_prompt').format(
content_language=content_language,
content_title='{content_title}',
content_outline='{content_outline}',
content_text='{content_text}',
web_research_result='{web_research_result}',
writing_guidelines=writing_guidelines
)
# Do SERP web research for given keywords to generate title and outline.
web_research_result, g_titles = do_google_serp_search(content_keywords)
# Generate prompts
try:
content_title = generate_with_retry(content_title.format(web_research_result=web_research_result))
logger.info(f"The title of the content is: {content_title}")
status.update(label=f"The title of the content is: {content_title}")
except Exception as err:
logger.error(f"Content title Generation Error: {err}")
return False
placeholder.success("Research completed successfully!")
return research_data
try:
content_outline = generate_with_retry(content_outline.format(
content_title=content_title,
web_research_result=web_research_result))
logger.info(f"The content Outline is: {content_outline}\n\n")
status.update(label=f"Completed with Content Outline.")
except Exception as err:
logger.error(f"Failed to generate content outline: {err}")
return False
try:
status.update(label=f"Do web research with Tavily to provide context for content creation.")
logger.info("Do web research with Tavily to provide context for content creation.")
# Do Metaphor/Exa AI search.
table_data = []
web_research_result, m_titles, t_titles = do_tavily_ai_search(content_keywords, max_results=5)
for item in web_research_result.get("results"):
title = item.get("title", "")
snippet = item.get("content", "")
table_data.append([title, snippet])
web_research_result = table_data
except Exception as err:
logger.error(f"Failed to do Tavily AI search: {err}")
st.error(f"Failed to do Tavily AI search: {err}")
return False
try:
starting_draft = generate_with_retry(starting_prompt.format(
content_title=content_title,
content_outline=content_outline,
web_research_result=web_research_result,
writing_guidelines=writing_guidelines))
except Exception as err:
st.error(f"Failed to Generate Starting draft: {err}")
logger.error(f"Failed to Generate Starting draft: {err}")
return False
try:
logger.info(f"Starting to write on the outline introduction.")
draft = starting_draft
continuation = generate_with_retry(continuation_prompt.format(
content_title=content_title,
content_outline=content_outline,
content_text=draft,
web_research_result=web_research_result,
writing_guidelines=writing_guidelines))
except Exception as err:
logger.error(f"Failed to write the initial draft: {err}")
return False
# Add the continuation to the initial draft, keep building the story until we see 'IAMDONE'
try:
draft += '\n\n' + continuation
except Exception as err:
logger.error(f"Failed as: {err} and {continuation}")
return False
logger.info(f"Writing in progress... Current draft length: {len(draft)} characters")
status.update(label=f"Writing in progress... Current draft length: {len(draft)} characters")
search_terms = f"""
I will provide you with content outline below, your task is to read the outline & return 8 google search keywords.
Your response will be used to do web research for writing on the given outline.
Do not explain your response, provide 8 google search sentences encompassing the given content outline.
Important: Provide the search term results as comma separated values.\n\n
Content Outline:\n
'{content_outline}'
"""
search_words = generate_with_retry(search_terms)
status.update(label=f"Search terms from written draft: {search_words}")
while 'IAMDONE' not in continuation:
#web_research_result, m_titles = do_metaphor_ai_research(content_keywords)
str_list = re.split(r',\s*', search_words)
# Strip quotes from each element
str_list = [s.strip('\'"') for s in str_list]
# for search_term in str_list:
# web_research_result, m_titles, t_titles = do_tavily_ai_search(search_term, max_results=5)
# status.update(label=f"Search terms from written draft: {search_term}")
# for item in web_research_result.get("results"):
# title = item.get("title", "")
# snippet = item.get("content", "")
# table_data.append([title, snippet])
# web_research_result = table_data
try:
continuation = generate_with_retry(continuation_prompt.format(
content_title=content_title,
content_outline=content_outline,
content_text=draft,
web_research_result=web_research_result,
writing_guidelines=writing_guidelines))
draft += '\n\n' + continuation
logger.info(f"Writing in progress... Current draft length: {len(draft)} characters")
status.update(label=f"Writing in progress... Current draft length: {len(draft)} characters")
# At this point, the context is little stale. We should more web research on
# related queries as per the content outline, to augment the LLM context.
except Exception as err:
st.error(f"Failed to continually write long-form content: {err}")
logger.error(f"Failed to continually write the Essay: {err}")
return False
# Remove 'IAMDONE' and print the final story
final = draft.replace('IAMDONE', '').strip()
status.update(label="Success: Finished writing Long form content.")
# # In long content sending the whole content for each content metadata is expensive.
# # https://ai.google.dev/gemini-api/docs/caching?lang=python
# #blog_title, blog_meta_desc, blog_tags, blog_categories = get_blog_metadata_longform(final)
# blog_categories = get_blog_metadata_longform(final)
# print("\n\n-----{blog_categories}------\n\n")
#
# status.update(label="Success: Finished with Title, Meta Description, Tags, categories")
# generated_image_filepath = None
# # TBD: Save the blog content as a .md file. Markdown or HTML ?
# save_blog_to_file(final, blog_title, blog_meta_desc, blog_tags, blog_categories, generated_image_filepath)
logger.info(f"\n{final}\n\n")
logger.info(f"\n\n ################ Finished writing Blog for : {content_keywords} #################### \n")
with st.expander("**Click to View the final content draft:**"):
st.markdown(f"\n{final}\n\n")
return final
except Exception as e:
placeholder.error(f"Research failed: {str(e)}")
return f"Unable to gather research for '{keywords}'. Please continue with the content based on your knowledge."
finally:
# Remove the placeholder after a short delay
import time
time.sleep(1)
placeholder.empty()
def generate_long_form_content(content_keywords):

View File

@@ -2,9 +2,116 @@ import streamlit as st
import streamlit.components.v1 as components
from typing import Dict, List
import json
import base64
from .tweet_generator import smart_tweet_generator
def add_bg_from_base64(base64_string):
"""Add background image using base64 string."""
return f'''
<style>
.stApp {{
background-image: url("data:image/png;base64,{base64_string}");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}}
/* Enhanced styling for containers */
.element-container, .stMarkdown, .stButton {{
background-color: rgba(0, 0, 0, 0.7);
border-radius: 10px;
padding: 20px;
margin: 10px 0;
backdrop-filter: blur(10px);
}}
/* Typography enhancements */
h1, h2, h3 {{
color: #ffffff !important;
font-family: 'Helvetica Neue', sans-serif;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}}
p, li {{
color: #e0e0e0 !important;
font-family: 'Helvetica Neue', sans-serif;
}}
/* Button styling */
.stButton > button {{
background: linear-gradient(45deg, #1DA1F2, #0C85D0);
color: white;
border: none;
padding: 10px 20px;
border-radius: 25px;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}}
.stButton > button:hover {{
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.2);
}}
/* Tab styling */
.stTabs [data-baseweb="tab-list"] {{
gap: 8px;
background-color: rgba(0, 0, 0, 0.6);
padding: 10px;
border-radius: 10px;
}}
.stTabs [data-baseweb="tab"] {{
background-color: transparent;
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 5px;
}}
.stTabs [data-baseweb="tab"]:hover {{
background-color: rgba(29, 161, 242, 0.2);
}}
/* Feature card styling */
.feature-card {{
background: linear-gradient(135deg, rgba(29, 161, 242, 0.1), rgba(0, 0, 0, 0.3));
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 15px;
margin-bottom: 20px;
transition: transform 0.3s ease;
}}
.feature-card:hover {{
transform: translateY(-5px);
}}
/* Status badge styling */
.status-badge {{
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
font-size: 0.8em;
font-weight: bold;
text-transform: uppercase;
}}
.status-active {{
background: linear-gradient(45deg, #00C853, #69F0AE);
color: #000000;
}}
.status-coming-soon {{
background: linear-gradient(45deg, #FFD700, #FFA000);
color: #000000;
}}
</style>
'''
def load_feature_data() -> Dict:
"""Load feature data from a structured format."""
return {
@@ -127,13 +234,13 @@ def load_feature_data() -> Dict:
def render_feature_card(feature: Dict) -> None:
"""Render a single feature card with its details."""
status_class = "status-active" if feature['status'] == 'active' else "status-coming-soon"
with st.container():
st.markdown(f"""
<div style='padding: 20px; border-radius: 10px; background-color: #f0f2f6; margin-bottom: 20px;'>
<h3 style='margin: 0;'>{feature['icon']} {feature['name']}</h3>
<p style='margin: 10px 0;'>{feature['description']}</p>
<span style='background-color: {'#4CAF50' if feature['status'] == 'active' else '#ffd700'};
padding: 5px 10px; border-radius: 15px; font-size: 0.8em;'>
<div class='feature-card'>
<h3 style='color: #ffffff; margin: 0;'>{feature['icon']} {feature['name']}</h3>
<p style='color: #e0e0e0; margin: 10px 0;'>{feature['description']}</p>
<span class='status-badge {status_class}'>
{feature['status'].title()}
</span>
</div>
@@ -150,23 +257,43 @@ def render_category_section(category: Dict) -> None:
with col2:
render_feature_card(category['features'][1])
def get_space_background() -> str:
"""Return base64 encoded space-themed background."""
return """iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN8/+F9PQAJYgN4hWvGzQAAAABJRU5ErkJggg==""" # This is a placeholder. You'll need to replace with actual base64 image
def run_dashboard():
"""Main function to run the Twitter dashboard."""
# Header
st.title("🐦 Twitter AI Writer Dashboard")
# Add space-themed background
st.markdown(add_bg_from_base64(get_space_background()), unsafe_allow_html=True)
# Enhanced Header with gradient text
st.markdown("""
Welcome to your all-in-one Twitter content creation and management platform.
Explore our AI-powered tools to enhance your Twitter marketing strategy.
""")
<div style='text-align: center; padding: 40px 0;'>
<h1 style='
font-size: 3em;
background: linear-gradient(45deg, #1DA1F2, #ffffff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 10px;
'>🐦 Twitter AI Writer</h1>
<p style='
font-size: 1.2em;
color: #e0e0e0;
max-width: 600px;
margin: 0 auto;
'>Your all-in-one Twitter content creation and management platform.
Harness the power of AI to enhance your Twitter marketing strategy.</p>
</div>
""", unsafe_allow_html=True)
# Load feature data
features = load_feature_data()
# Create tabs for different sections
# Create tabs with enhanced styling
tab1, tab2, tab3 = st.tabs(["🎯 Quick Actions", "📊 Analytics", "⚙️ Settings"])
with tab1:
st.markdown("### 🚀 Quick Actions")
st.markdown("<h2 style='color: #ffffff;'>🚀 Quick Actions</h2>", unsafe_allow_html=True)
col1, col2, col3 = st.columns(3)
with col1:
@@ -199,11 +326,29 @@ def run_dashboard():
if st.button(f"🚀 Launch {category['features'][0]['name']}", use_container_width=True):
category["features"][0]["function"]()
# Footer
# Enhanced Footer
st.markdown("---")
st.markdown("""
<div style='text-align: center;'>
<p>Need help? Check out our <a href='#'>documentation</a> or <a href='#'>contact support</a></p>
<div style='text-align: center; padding: 20px; background: rgba(0, 0, 0, 0.5); border-radius: 10px;'>
<p style='color: #ffffff; margin-bottom: 10px;'>Need assistance? We're here to help!</p>
<div style='display: flex; justify-content: center; gap: 20px;'>
<a href='#' style='
text-decoration: none;
color: #1DA1F2;
background: rgba(255, 255, 255, 0.1);
padding: 8px 20px;
border-radius: 20px;
transition: all 0.3s ease;
'>📚 Documentation</a>
<a href='#' style='
text-decoration: none;
color: #1DA1F2;
background: rgba(255, 255, 255, 0.1);
padding: 8px 20px;
border-radius: 20px;
transition: all 0.3s ease;
'>💬 Contact Support</a>
</div>
</div>
""", unsafe_allow_html=True)

View File

@@ -34,16 +34,53 @@ def llm_text_gen(prompt, system_prompt=None, json_struct=None):
logger.debug(f"[llm_text_gen] Prompt length: {len(prompt)} characters")
try:
# Read the config param to create system instruction for the LLM.
gpt_provider, model, temperature, max_tokens, top_p, n, fp = read_return_config_section('llm_config')
blog_tone, blog_demographic, blog_type, blog_language, \
blog_output_format, blog_length = read_return_config_section('blog_characteristics')
# Set default values for LLM parameters
gpt_provider = "google"
model = "gemini-1.5-flash-latest"
temperature = 0.7
max_tokens = 4000
top_p = 0.9
n = 1
fp = 16
logger.debug(f"[llm_text_gen] Config loaded successfully - Provider: {gpt_provider}, Model: {model}")
# Default blog characteristics
blog_tone = "Professional"
blog_demographic = "Professional"
blog_type = "Informational"
blog_language = "English"
blog_output_format = "markdown"
blog_length = 2000
# Try to read values from config, but keep defaults if any key is missing
try:
# Read LLM config
llm_config = read_return_config_section('llm_config')
if llm_config and len(llm_config) >= 4:
gpt_provider = llm_config[0] if llm_config[0] else gpt_provider
model = llm_config[1] if llm_config[1] else model
temperature = llm_config[2] if llm_config[2] else temperature
max_tokens = llm_config[3] if llm_config[3] else max_tokens
# Use default values for top_p, n, fp if they're not in the config
logger.debug(f"[llm_text_gen] LLM Config loaded: Provider={gpt_provider}, Model={model}, Temp={temperature}")
except Exception as err:
logger.warning(f"[llm_text_gen] Couldn't load LLM config completely, using defaults where needed: {err}")
try:
# Read blog characteristics
blog_chars = read_return_config_section('blog_characteristics')
if blog_chars and len(blog_chars) >= 6:
blog_tone = blog_chars[0] if blog_chars[0] else blog_tone
blog_demographic = blog_chars[1] if blog_chars[1] else blog_demographic
blog_type = blog_chars[2] if blog_chars[2] else blog_type
blog_language = blog_chars[3] if blog_chars[3] else blog_language
blog_output_format = blog_chars[4] if blog_chars[4] else blog_output_format
blog_length = blog_chars[5] if blog_chars[5] else blog_length
logger.debug(f"[llm_text_gen] Blog characteristics loaded: Tone={blog_tone}, Type={blog_type}")
except Exception as err:
logger.warning(f"[llm_text_gen] Couldn't load blog characteristics completely, using defaults where needed: {err}")
except Exception as err:
logger.error(f"[llm_text_gen] Error reading config params: {err}")
raise err
logger.warning(f"[llm_text_gen] Using default settings due to config read error: {err}")
# Construct the system prompt with the sidebar config params if no custom system_prompt is provided
if system_prompt is None:
@@ -110,9 +147,13 @@ def llm_text_gen(prompt, system_prompt=None, json_struct=None):
except Exception as err:
logger.error(f"Failed to get response from DeepSeek: {err}")
raise err
else:
logger.warning(f"Unknown provider '{gpt_provider}', falling back to Google Gemini")
response = gemini_text_response(prompt, temperature, top_p, n, max_tokens, system_instructions)
return response
except Exception as err:
logger.error(f"Failed to read LLM parameters: {err}")
logger.error(f"Failed to generate text: {err}")
raise

View File

@@ -1,272 +1,25 @@
import re
import os
import PyPDF2
import tiktoken
import openai
import streamlit as st
import tempfile
from loguru import logger
from lib.ai_web_researcher.gpt_online_researcher import gpt_web_researcher
from lib.ai_writers.keywords_to_blog_streamlit import write_blog_from_keywords
from lib.ai_writers.speech_to_blog.main_audio_to_blog import generate_audio_blog
from lib.ai_writers.long_form_ai_writer import long_form_generator
from lib.ai_writers.ai_news_article_writer import ai_news_generation
#from lib.ai_writers.ai_agents_crew_writer import ai_agents_writers
from lib.ai_writers.ai_financial_writer import write_basic_ta_report
from lib.ai_writers.ai_facebook_writer.facebook_ai_writer import facebook_main_menu
from lib.ai_writers.linkedin_writer.linkedin_ai_writer import linkedin_main_menu
from lib.ai_writers.twitter_writers.twitter_dashboard import run_dashboard
from lib.ai_writers.insta_ai_writer import insta_writer
from lib.ai_writers.youtube_writers.youtube_ai_writer import youtube_main_menu
from lib.ai_writers.web_url_ai_writer import blog_from_url
from lib.ai_writers.image_ai_writer import blog_from_image
from lib.ai_writers.ai_essay_writer import ai_essay_generator
from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
from lib.utils.voice_processing import record_voice
#from lib.content_planning_calender.content_planning_agents_alwrity_crew import ai_agents_content_planner
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
def is_youtube_link(text):
if text is not None:
youtube_regex = re.compile(r'(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})')
return youtube_regex.match(text)
def is_web_link(text):
if text is not None:
web_regex = re.compile(r'(https?://)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)')
return web_regex.match(text)
def process_input(input_text, uploaded_file):
if input_text and is_youtube_link(input_text):
if input_text.startswith("https://www.youtube.com/") or input_text.startswith("http://www.youtube.com/"):
return "youtube_url"
else:
st.error("Invalid YouTube URL. Please enter a valid URL.")
return None
elif input_text and is_web_link(input_text):
return "web_url"
elif input_text:
return "keywords"
if uploaded_file is not None:
file_details = {"filename": uploaded_file.name, "filetype": uploaded_file.type}
st.write(file_details)
if uploaded_file.type.startswith("text/"):
content = uploaded_file.read().decode("utf-8")
st.text(content)
elif uploaded_file.type == "application/pdf":
return "PDF_file"
elif uploaded_file.type in ["application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/msword"]:
st.write("Word document uploaded. Add your DOCX processing logic here.")
elif uploaded_file.type.startswith("image/"):
st.image(uploaded_file)
return "image_file"
elif uploaded_file.type.startswith("audio/"):
st.audio(uploaded_file)
return "audio_file"
elif uploaded_file.type.startswith("video/"):
st.video(uploaded_file)
return "video_file"
return None
def blog_from_keyword():
""" Input blog keywords, research and write a factual blog."""
st.header("Blog Content Writer")
col1, col2, col3 = st.columns([2, 1.5, 0.5])
with col1:
user_input = st.text_area('**👇Enter Keywords/Title/YouTube Link/Web URLs**',
help='Provide keywords, titles, YouTube links, or web URLs to generate content.',
placeholder="""Write Blog From:
- Keywords/Blog Title: Provide keywords to web research & write blog.
- Attach file: Attach Text, Audio, Video, Image file to blog on.
- YouTube Link: Provide a YouTube video link to convert into blog.
- Web URLs: Provide web URL to write similar blog on.
- Provide Local folder location with your documents to use for content creation.""")
with col2:
uploaded_file = st.file_uploader("**👇Attach files (Audio, Video, Image, Document)**",
type=["txt", "pdf", "docx", "jpg", "jpeg", "png", "mp3", "wav", "mp4", "mkv", "avi"],
help='Attach files such as audio, video, images, or documents.')
with col3:
audio_input = record_voice()
if audio_input:
st.info(audio_input)
# Validate the provided folder path
#st.info("🚨 Currently supported file formats are: PDF, plain text, CSV, Excel, Markdown, PowerPoint, and Word documents.")
temp_file_path = None
if uploaded_file is not None:
# Save the uploaded file to a temporary file
with tempfile.NamedTemporaryFile(delete=False, suffix=uploaded_file.name) as temp_file:
temp_file.write(uploaded_file.read())
temp_file_path = temp_file.name
content_type = st.radio("**👇Select content type:**", ["Normal-length content", "Long-form content", "Experimental - AI Agents team"])
# Add an expandable section for advanced writing options
with st.expander("Advanced Writing Options", expanded=False):
# Option 1: Select content type
content_type = st.radio("**👇 Select content type:**",
["Normal-length content", "Long-form content", "Experimental - AI Agents team"])
# Option 2: Checkbox for 'Create SEO tags' (Checked by default)
create_seo_tags = st.checkbox('Create SEO tags', value=True,
help='Generate json-ld schema, Twitter, and Facebook tags.')
# Option 3: Checkbox for 'Generate Social Media content' (Unchecked by default)
generate_social_media = st.checkbox('Generate Social Media content', value=False,
help="Write Facebook, Instagram posts & tweets for generated blog. Needed for marketing your blogs.")
# Option 4: Checkbox for 'Do Content Analysis & Critique' (Unchecked by default)
content_analysis = st.checkbox('Do Content Analysis & Critique', value=False,
help="Blog Proof reading, Critique generated blog. Provide actionable changes & Editing options.")
# Display a message at the bottom for user guidance
st.info("🚨 Make sure to personalize content from the sidebar. Important.")
if st.button("Write Blog"):
# Clear the previous results from the screen
st.empty()
if user_input == "":
user_input = None
if not uploaded_file and not user_input and not audio_input:
st.error("🤬🤬 Either Enter/Type/Attach, can't read your mind.(yet..)")
st.stop()
else:
input_type = process_input(user_input, uploaded_file)
if input_type == "keywords":
if user_input and len(user_input.split()) >= 2:
if content_type == "Normal-length content":
try:
short_blog = write_blog_from_keywords(user_input)
st.markdown(short_blog)
except Exception as err:
st.error(f"🚫 Failed to write blog on {user_input}, Error: {err}")
elif content_type == "Long-form content":
try:
long_form_generator(user_input)
st.success(f"Successfully wrote long-form blog on: {user_input}")
except Exception as err:
st.error(f"🚫 Failed to write blog on {user_input}, Error: {err}")
elif content_type == "Experimental - AI Agents team":
try:
ai_agents_writers(user_input)
st.success(f"Successfully wrote content with AI agents on: {user_input}")
except Exception as err:
st.error(f"🚫 Failed to Write content with AI agents: {err}")
else:
st.error('🚫 Blog keywords should be at least two words long. Please try again.')
elif input_type == "youtube_url" or input_type == "audio_file":
if not generate_audio_blog(user_input):
st.stop()
elif input_type == "web_url":
blog_from_url(user_input)
elif input_type == "image_file":
blog_from_image(user_input, temp_file_path)
elif input_type == "PDF_file":
pdf_reader = PyPDF2.PdfReader(uploaded_file)
text = ""
combined_result = ""
# Create a placeholder for the progress bar
progress_bar = st.progress(0)
# Loop through each page with a progress bar
for page_num, page in enumerate(pdf_reader.pages):
text += page.extract_text()
# Replace newlines with spaces
text = text.replace("\n", " ")
# Use regex to add a space between words that are combined
text = re.sub(r"(\w)([A-Z])", r"\1 \2", text)
results = blog_from_pdf(text)
# Update the progress bar
progress_bar.progress((page_num + 1) / len(pdf_reader.pages))
combined_result += str(results[-1])
# Clear progress bar at the end
progress_bar.empty()
st.markdown(combined_result)
def blog_from_pdf(pdf_text):
""" Load in a long PDF and pull the text out. Create a prompt to be used to extract key bits of information.
Chunk up our document and process each chunk to pull any answers out. Combine them at the end.
This simple approach will then be extended to three more difficult questions.
"""
# FixME:
document = '<document>'
template_prompt=f'''Extract key pieces of information from the given document.
When you extract a key piece of information, include the closest page number.
Ex: Extracted Information (Page number)
\n\nDocument: \"\"\"<document>\"\"\"\n\n'''
# Initialise tokenizer
tokenizer = tiktoken.get_encoding("cl100k_base")
results = []
chunks = create_chunks(pdf_text, 1000, tokenizer)
text_chunks = [tokenizer.decode(chunk) for chunk in chunks]
for chunk in text_chunks:
results.append(extract_chunk(chunk, template_prompt))
#zipped = list(zip(*groups))
#zipped = [x for y in zipped for x in y if "Not specified" not in x and "__" not in x]
return results
# Split a text into smaller chunks of size n, preferably ending at the end of a sentence
def create_chunks(text, n, tokenizer):
tokens = tokenizer.encode(text)
"""Yield successive n-sized chunks from text."""
i = 0
while i < len(tokens):
# Find the nearest end of sentence within a range of 0.5 * n and 1.5 * n tokens
j = min(i + int(1.5 * n), len(tokens))
while j > i + int(0.5 * n):
# Decode the tokens and check for full stop or newline
chunk = tokenizer.decode(tokens[i:j])
if chunk.endswith(".") or chunk.endswith("\n"):
break
j -= 1
# If no end of sentence found, use n tokens as the chunk size
if j == i + int(0.5 * n):
j = min(i + n, len(tokens))
yield tokens[i:j]
i = j
def extract_chunk(document, template_prompt):
""" Chunking for large documents, exceed context window"""
client = openai.OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
prompt = template_prompt.replace('<document>', document)
try:
response = llm_text_gen(prompt)
return response
except Exception as err:
logger.error(f"Exit: Failed to get response from LLM: {err}")
exit(1)
def ai_agents_team():
# Define options for AI Content Teams
st.title("🐲 Your AI Agents Teams")
@@ -316,9 +69,9 @@ def content_agents():
if content_keywords and len(content_keywords.split()) >= 2:
with st.spinner("Generating Content..."):
try:
calendar_content = ai_agents_writers(content_keywords)
st.success(f"Successfully generated content for: {content_keywords}")
st.markdown(calendar_content)
#calendar_content = ai_agents_writers(content_keywords)
st.success(f"🚫 Not implemented yet: {content_keywords}")
#st.markdown(calendar_content)
except Exception as err:
st.error(f"🚫 Failed to generate content with AI Agents: {err}")
else:
@@ -474,4 +227,4 @@ def ai_social_writer():
elif "instagram" in selected_platform:
insta_writer()
elif "youtube" in selected_platform:
youtube_main_menu()
youtube_main_menu()

View File

@@ -1,57 +1,11 @@
import streamlit as st
from lib.utils.alwrity_utils import (
blog_from_keyword, ai_agents_team, essay_writer, ai_news_writer,
ai_finance_ta_writer
)
from lib.alwrity_ui.similar_analysis import competitor_analysis
from lib.alwrity_ui.keyword_web_researcher import do_web_research
from lib.ai_writers.ai_story_writer.story_writer import story_input_section
from lib.ai_writers.ai_product_description_writer import write_ai_prod_desc
from lib.ai_writers.ai_copywriter.copywriter_dashboard import copywriter_dashboard
from lib.ai_writers.linkedin_writer import LinkedInAIWriter
#from lib.content_planning_calender.content_planning_agents_alwrity_crew import ai_agents_content_planner
def ai_writers():
options = [
"AI Blog Writer",
"Story Writer",
"Essay writer",
"Write News reports",
"Write Financial TA report",
"AI Product Description Writer",
"AI Copywriter",
"LinkedIn AI Writer",
"Quit"
]
choice = st.selectbox("**👇Select a content creation type:**", options, index=0, format_func=lambda x: f"📝 {x}")
if choice == "AI Blog Writer":
blog_from_keyword()
elif choice == "Story Writer":
story_input_section()
elif choice == "Essay writer":
essay_writer()
elif choice == "Write News reports":
ai_news_writer()
elif choice == "Write Financial TA report":
ai_finance_ta_writer()
elif choice == "AI Product Description Writer":
write_ai_prod_desc()
elif choice == "AI Copywriter":
# Initialize the copywriter dashboard
copywriter_dashboard()
elif choice == "LinkedIn AI Writer":
# Initialize the LinkedIn AI Writer
linkedin_writer = LinkedInAIWriter()
linkedin_writer.run()
elif choice == "Quit":
st.info("Thank you for using Alwrity. Goodbye!")
st.stop()
def content_planning_tools():
# Add custom CSS for compact layout
# A custom CSS for compact layout
st.markdown("""
<style>
/* Reduce top padding of main container */

View File

@@ -1,10 +1,11 @@
import os
import streamlit as st
from lib.utils.file_processor import load_image
from lib.utils.content_generators import content_planning_tools, ai_writers
from lib.utils.content_generators import content_planning_tools
from lib.utils.alwrity_utils import ai_social_writer
from lib.utils.seo_tools import ai_seo_tools
from lib.utils.settings_page import render_settings_page
from loguru import logger
# Import social media writer functions
from lib.ai_writers.ai_facebook_writer.facebook_ai_writer import facebook_main_menu
@@ -12,6 +13,7 @@ from lib.ai_writers.linkedin_writer.linkedin_ai_writer import linkedin_main_menu
from lib.ai_writers.twitter_writers import run_dashboard
from lib.ai_writers.insta_ai_writer import insta_writer
from lib.ai_writers.youtube_writers.youtube_ai_writer import youtube_main_menu
from lib.ai_writers.ai_writer_dashboard import get_ai_writers, list_ai_writers
def setup_ui():
@@ -295,22 +297,26 @@ def setup_ui():
def setup_alwrity_ui():
"""Sets up the main navigation in the sidebar."""
logger.info("Setting up ALwrity UI")
# Initialize session state for active tab if not exists
if 'active_tab' not in st.session_state:
st.session_state.active_tab = "Content Planning"
logger.info(f"Initialized active_tab to: {st.session_state.active_tab}")
# Initialize session state for active sub-tab if not exists
if 'active_sub_tab' not in st.session_state:
st.session_state.active_sub_tab = None
logger.info("Initialized active_sub_tab to None")
# Define the navigation items with their icons and functions
nav_items = {
"AI Writers": ("📝", get_ai_writers),
"Content Planning": ("📅", content_planning_tools),
"AI Writers": ("📝", ai_writers),
"Agents Teams": ("🤝", lambda: st.subheader("Agents Teams - Coming Soon!")),
"AI SEO Tools": ("🔍", ai_seo_tools),
"AI Social Tools": ("📱", None), # Set to None as we'll handle this separately
"Ask Alwrity": ("💬", lambda: (
"Agents Teams(TBD)": ("🤝", lambda: st.subheader("Agents Teams - Coming Soon!")),
"Ask Alwrity(TBD)": ("💬", lambda: (
st.subheader("Chat with your Data, Chat with any Data.. COMING SOON !"),
st.markdown("Create a collection by uploading files (PDF, MD, CSV, etc), or crawl a data source (Websites, more sources coming soon."),
st.markdown("One can ask/chat, summarize and do semantic search over the uploaded data")
@@ -318,6 +324,8 @@ def setup_alwrity_ui():
"ALwrity Settings": ("⚙️", render_settings_page)
}
logger.info(f"Defined {len(nav_items)} navigation items")
# Define sub-menu items for AI Social Tools
social_tools_submenu = {
"Facebook": ("📘", lambda: facebook_main_menu()),
@@ -326,6 +334,8 @@ def setup_alwrity_ui():
"Instagram": ("📸", lambda: insta_writer()),
"YouTube": ("🎥", lambda: youtube_main_menu())
}
logger.info(f"Defined {len(social_tools_submenu)} social tools submenu items")
# Create sidebar navigation
st.sidebar.markdown("### ALwrity Options")
@@ -342,6 +352,7 @@ def setup_alwrity_ui():
st.session_state.active_tab = name
# Reset sub-tab when main tab changes
st.session_state.active_sub_tab = None
logger.info(f"Selected main tab: {name}")
# If AI Social Tools is active, show the sub-menu
if st.session_state.active_tab == "AI Social Tools":
@@ -367,6 +378,7 @@ def setup_alwrity_ui():
if st.sidebar.button(f"{sub_icon} {sub_name}", key=button_key,
help=f"Navigate to {sub_name}", use_container_width=True):
st.session_state.active_sub_tab = sub_name
logger.info(f"Selected social tool: {sub_name}")
# Close the div
st.sidebar.markdown('</div>', unsafe_allow_html=True)
@@ -379,6 +391,7 @@ def setup_alwrity_ui():
st.session_state.active_tab = name
# Reset sub-tab when main tab changes
st.session_state.active_sub_tab = None
logger.info(f"Selected main tab: {name}")
st.sidebar.markdown('</div>', unsafe_allow_html=True)
@@ -427,13 +440,47 @@ def setup_alwrity_ui():
# Call the function directly without any title
social_tools_submenu[st.session_state.active_sub_tab][1]()
else:
st.markdown("""
<style>
.main .block-container {
padding-top: 0.25rem !important;
padding-bottom: 0;
}
</style>
""", unsafe_allow_html=True)
st.title(f"{nav_items[st.session_state.active_tab][0]} {st.session_state.active_tab}")
nav_items[st.session_state.active_tab][1]()
# Check if we're in the AI Writers section and handle writer selection
if st.session_state.active_tab == "AI Writers":
# Get the writer parameter from the URL using st.query_params
writer = st.query_params.get("writer")
logger.info(f"Current writer from query params: {writer}")
if writer:
# Get the list of writers without rendering the dashboard
writers = list_ai_writers()
logger.info(f"Found {len(writers)} writers")
writer_found = False
for w in writers:
logger.info(f"Checking writer: {w['name']} with path: {w['path']}")
if w["path"] == writer:
writer_found = True
logger.info(f"Found matching writer: {w['name']}, executing function")
# Clear any existing content
st.empty()
# Execute the writer function
w["function"]()
break
if not writer_found:
logger.error(f"No writer found with path: {writer}")
st.error(f"No writer found with path: {writer}")
else:
# If no writer selected, show the dashboard
logger.info("No writer selected, showing dashboard")
get_ai_writers()
else:
# For all other tabs, show the title
st.markdown("""
<style>
.main .block-container {
padding-top: 0.25rem !important;
padding-bottom: 0;
}
</style>
""", unsafe_allow_html=True)
st.title(f"{nav_items[st.session_state.active_tab][0]} {st.session_state.active_tab}")
nav_items[st.session_state.active_tab][1]()
logger.info("Finished setting up ALwrity UI")

View File

@@ -16,7 +16,10 @@
"GPT Provider": "google",
"Model": "gemini-1.5-flash-latest",
"Temperature": 0.7,
"Max Tokens": 4000
"Max Tokens": 4000,
"Top-p": 0.9,
"n": 1,
"fp": 16
},
"Search Engine Parameters": {
"Geographic Location": "us",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 946 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 694 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 879 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 742 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 838 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 771 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 794 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 831 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 880 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 877 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 789 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 805 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 898 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 KiB

View File

@@ -1,3 +0,0 @@
Alwrity web research reports will be saved in this folder.
You can change this by modifying SEARCH_SAVE_FILE environment variable, after running alwrity.py from the command prompt.
Better to change in the alwrity.py file itself, line no: 308