vivekchakraverty's picture
Fix empty-content crash with reasoning models (GLM-5.2 default)
d4ab656
Raw
History Blame Contribute Delete
10.4 kB
"""Gradio UI for the AI Digital Marketing Plan Generator.
All LLM calls are billed to the user's own Hugging Face token, entered in a
password-style box below and used in-memory only for the duration of a
request — never logged, stored, or persisted.
"""
from __future__ import annotations
import json
import re
import tempfile
from pathlib import Path
import gradio as gr
from modules import ads, composer, keywords, llm, rag, seo, social
AUTO_MODEL_LABEL = "Auto (recommended model per task)"
# Load the RAG index (downloading it from RAG_DATASET_ID if not present locally)
# at container startup rather than on the first user request — trades a
# slower cold start for no first-request latency spike, and surfaces a
# download/credentials failure immediately in the startup logs instead of
# silently during someone's first plan generation.
print("[startup] Loading RAG index...")
if rag.is_available():
print(f"[startup] RAG index loaded: {rag.chunk_count()} chunks.")
else:
print("[startup] RAG index NOT available — plans will be generated without RAG grounding.")
INDUSTRY_LABELS = {
"ecommerce_retail": "Ecommerce / Retail",
"apparel_fashion": "Apparel / Fashion",
"b2b_saas": "B2B SaaS",
"technology_electronics": "Technology / Electronics",
"education": "Education",
"finance_insurance": "Finance / Insurance",
"health_medical": "Health / Medical",
"home_improvement": "Home Improvement",
"legal": "Legal",
"real_estate": "Real Estate",
"travel_hospitality": "Travel / Hospitality",
"automotive": "Automotive",
"beauty_personal_care": "Beauty / Personal Care",
"restaurants_food": "Restaurants / Food",
"fitness_wellness": "Fitness / Wellness",
"nonprofit": "Nonprofit",
"professional_services": "Professional Services",
"furniture_home_goods": "Furniture / Home Goods",
"industrial_manufacturing": "Industrial / Manufacturing",
"consumer_services": "Consumer Services",
}
INDUSTRY_CHOICES = [(label, key) for key, label in INDUSTRY_LABELS.items()]
EXAMPLES = [
[
"Handmade full-grain leather laptop bags and backpacks, sold direct-to-consumer online.",
2000,
"2 people: 1 generalist marketer (full-time), 1 designer (10 hrs/week)",
"apparel_fashion",
"US",
],
[
"A B2B SaaS tool that automates expense report approvals for mid-size companies.",
8000,
"3 people: 1 growth marketer, 1 content writer, 1 part-time designer",
"b2b_saas",
"US",
],
]
def _derive_seed_keywords(hf_token: str, model: str, product_description: str) -> list[str]:
prompt = f"""Given this product/service description, list 8-12 seed keywords a potential
customer might search for. Respond ONLY with a JSON array of strings, no other text.
Product/service: {product_description}
"""
raw = llm.chat(
hf_token=hf_token,
model=model,
messages=[{"role": "user", "content": prompt}],
# The answer itself is ~100 tokens, but reasoning models (the default
# GLM-5.2 included) think before answering and that counts against
# max_tokens — 400 left no room and made content come back empty.
max_tokens=2000,
temperature=0.3,
)
match = re.search(r"\[.*\]", raw, re.DOTALL)
if not match:
raise llm.LLMError("Could not parse seed keywords from the model's response.")
return json.loads(match.group(0))
def _keyword_sources_note(keyword_data: list[keywords.KeywordData]) -> str:
sources = sorted({kd.source for kd in keyword_data})
labels = {
"google_ads_api": "Google Ads API (official)",
"keyword_surfer": "live Keyword Surfer scrape",
"autocomplete_trends": "Google Autocomplete + Trends (estimated)",
"llm_estimate": "LLM estimate (no live data available)",
}
return ", ".join(labels.get(s, s) for s in sources) if sources else "no keyword data available"
def generate_plan(
product_description: str,
budget_usd_per_month: float,
manpower_summary: str,
industry_key: str,
geo: str,
hf_token: str,
model: str,
):
# "Auto" lets each module use its own recommended model (SEO/planning,
# social/creative-writing, ads/quantitative each favor a different model —
# see RECOMMENDED_MODEL in seo.py / social.py / ads.py). Picking a specific
# model here overrides all tasks with that one model instead.
selected_model = None if model == AUTO_MODEL_LABEL else model
status = ""
seo_md, social_md, ads_md, full_md = "", "", "", ""
download_path = None
def state():
return status, seo_md, social_md, ads_md, full_md, download_path
if not product_description or not product_description.strip():
status = "Please describe your product or service."
yield state()
return
if not hf_token or not hf_token.strip():
status = "Please enter your Hugging Face access token."
yield state()
return
utility_model = selected_model or llm.DEFAULT_MODEL
try:
status = "Deriving seed keywords from your product description..."
yield state()
seed_keywords = _derive_seed_keywords(hf_token, utility_model, product_description)
status = f"Researching {len(seed_keywords)} keywords (this may take a minute)..."
yield state()
keyword_data = keywords.research_keywords(seed_keywords, hf_token, utility_model, geo=geo)
keyword_source_note = _keyword_sources_note(keyword_data)
status = f"Building SEO plan (keyword data: {keyword_source_note})..."
yield state()
seo_md = seo.build_seo_plan(
hf_token, product_description, manpower_summary, keyword_data, model=selected_model
)
yield state()
status = "Building organic social media plan..."
yield state()
social_md = social.build_social_plan(
hf_token,
product_description,
manpower_summary,
INDUSTRY_LABELS.get(industry_key, industry_key),
geo,
industry_key=industry_key,
model=selected_model,
)
yield state()
status = "Building paid advertising plan..."
yield state()
ads_md = ads.build_ads_plan(
hf_token,
product_description,
float(budget_usd_per_month or 0),
manpower_summary,
industry_key,
geo,
keyword_data=keyword_data,
model=selected_model,
)
yield state()
status = "Composing the final plan (retrieving grounding context)..."
yield state()
full_md = composer.compose_plan(
hf_token,
product_description,
float(budget_usd_per_month or 0),
manpower_summary,
INDUSTRY_LABELS.get(industry_key, industry_key),
geo,
seo_md,
ads_md,
social_md,
model=selected_model,
)
tmp_dir = Path(tempfile.mkdtemp(prefix="dmplan_"))
download_path = str(tmp_dir / "digital_marketing_plan.md")
Path(download_path).write_text(full_md, encoding="utf-8")
status = f"Done. Keyword data source: {keyword_source_note}."
yield state()
except llm.LLMError as exc:
status = f"Error: {exc}"
yield state()
except Exception as exc: # keep the UI alive on unexpected failures
status = f"Unexpected error: {exc}"
yield state()
with gr.Blocks(title="AI Digital Marketing Plan Generator") as demo:
gr.Markdown(
"# AI Digital Marketing Plan Generator\n"
"Free tool — **LLM calls are billed to your own Hugging Face token**, entered below. "
"Your token is used in-memory only and never stored.\n\n"
"Get a token with Inference Providers billing enabled at "
"[huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)."
)
with gr.Row():
with gr.Column(scale=1):
product_description = gr.Textbox(
label="Product / service description",
lines=4,
placeholder="e.g. Handmade full-grain leather laptop bags, sold direct-to-consumer online.",
)
budget = gr.Number(label="Monthly marketing budget (USD)", value=2000, minimum=0)
manpower = gr.Textbox(
label="Available manpower",
placeholder="e.g. 2 people: 1 generalist marketer full-time, 1 designer 10 hrs/week",
)
industry = gr.Dropdown(
label="Industry", choices=INDUSTRY_CHOICES, value="ecommerce_retail"
)
geo = gr.Textbox(label="Geography (country code, optional)", placeholder="e.g. US")
hf_token = gr.Textbox(
label="Hugging Face access token", type="password", placeholder="hf_..."
)
model = gr.Dropdown(
label="Model",
choices=[AUTO_MODEL_LABEL] + llm.AVAILABLE_MODELS,
value=AUTO_MODEL_LABEL,
info="Auto picks a different best-fit model per task (SEO/social/ads each favor a different one) — override to force one model for everything.",
)
generate_btn = gr.Button("Generate Plan", variant="primary")
status = gr.Markdown()
with gr.Column(scale=2):
with gr.Tabs():
with gr.Tab("Full Plan"):
full_plan_out = gr.Markdown()
download_btn = gr.File(label="Download plan (.md)")
with gr.Tab("SEO Plan"):
seo_out = gr.Markdown()
with gr.Tab("Social Plan"):
social_out = gr.Markdown()
with gr.Tab("Ads Plan"):
ads_out = gr.Markdown()
gr.Examples(
examples=EXAMPLES,
inputs=[product_description, budget, manpower, industry, geo],
)
generate_btn.click(
fn=generate_plan,
inputs=[product_description, budget, manpower, industry, geo, hf_token, model],
outputs=[status, seo_out, social_out, ads_out, full_plan_out, download_btn],
)
if __name__ == "__main__":
demo.queue().launch(server_name="0.0.0.0", server_port=7860)