decodingdatascience's picture
Update app.py
d480446 verified
raw
history blame
14 kB
# app.py
from crewai import Agent, Task, Crew
import gradio as gr
import re
from datetime import datetime
from pathlib import Path
import os
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
# --- Logo: convert GitHub page URL to raw if needed ---
def to_raw_github(url: str) -> str:
# Accepts both blob and raw URLs; converts blob β†’ raw
return url.replace("https://github.com/", "https://raw.githubusercontent.com/").replace("/blob/", "/")
LOGO_URL = to_raw_github("https://github.com/Decoding-Data-Science/airesidency/blob/main/dds_logo.jpg")
# ----------------------------
# Agents
# ----------------------------
lead_market_analyst = Agent(
role="Lead Market Analyst",
goal="Deliver sharp, data-driven market insights for the brand/product.",
backstory=("Senior analyst skilled in competitor intelligence, audience segmentation, "
"channel dynamics, and market sizing. Outputs actionable insights."),
allow_delegation=False,
verbose=True,
)
chief_marketing_strategist = Agent(
role="Chief Marketing Strategist",
goal="Turn research into a focused, measurable go-to-market strategy.",
backstory=("Veteran strategist who crafts positioning, messaging pillars, channel mix, "
"and KPI frameworks; coordinates the team."),
allow_delegation=False,
verbose=True,
)
creative_content_creator = Agent(
role="Creative Content Creator",
goal="Transform the strategy into compelling campaign concepts and a content calendar.",
backstory=("Concept-to-copy creative converting strategy into campaign ideas, ad copy, "
"social posts, and long-form content."),
allow_delegation=False,
verbose=True,
)
# Optional: focused social copywriter (only if toggled)
social_copywriter = Agent(
role="Social Copywriter",
goal="Turn strategy into platform-appropriate copy with strong hooks and clear CTAs.",
backstory=("Skilled at LinkedIn thought-leadership, X brevity, and long-form posts that convert."),
allow_delegation=False,
verbose=False,
)
# ----------------------------
# Core crew
# ----------------------------
def run_marketing_crew(product_brand: str, target_audience: str, objective: str) -> str:
topic = f"{product_brand} | Audience: {target_audience} | Objective: {objective}"
market_analysis_task = Task(
description=(
f"Conduct a concise market analysis for: {topic}. "
"Cover: (1) ICP & segments, (2) JTBD/pain points & objections, "
"(3) competitive landscape & whitespace, (4) demand signals & seasonality, "
"(5) channel dynamics (search/social/email/partners/events), "
"(6) keyword themes & content gaps, (7) risks/assumptions."
),
expected_output=(
"A structured brief with bullet points for each section and a 5–8 point summary of "
"the most actionable insights."
),
agent=lead_market_analyst
)
strategy_task = Task(
description=(
f"Using the Market Analysis brief, craft a go-to-market strategy for: {topic}. "
"Include: positioning statement, value prop, 3–5 messaging pillars, "
"priority segments, channel mix with rationale, offer/CTA ideas, "
"90-day roadmap (phases & owners), KPI tree (primary/leading indicators), "
"and a lightweight budget allocation (% by channel)."
),
expected_output="A strategy document with the sections above plus a one-page executive summary.",
agent=chief_marketing_strategist,
context=[market_analysis_task]
)
creative_task = Task(
description=(
"Based on the Strategy, produce: (a) 3 campaign concepts (hook, angle, proof), "
"(b) ad copy variants (paid search & paid social), "
"(c) a 4-week content calendar (blog/LinkedIn/X/YouTube/Newsletter) with titles, "
"briefs, CTAs, intended KPIs, and (d) a landing-page wireframe outline "
"(hero, value blocks, social proof, FAQ)."
),
expected_output="Campaign concepts + copy, a tabular content calendar, and a structured LP outline.",
agent=creative_content_creator,
context=[strategy_task]
)
crew = Crew(
agents=[lead_market_analyst, chief_marketing_strategist, creative_content_creator],
tasks=[market_analysis_task, strategy_task, creative_task],
verbose=True
)
return crew.kickoff()
# ----------------------------
# Helpers
# ----------------------------
def _first_n_points(text: str, n: int = 5):
lines = [l.strip() for l in text.splitlines() if l.strip()]
bullets = []
for l in lines:
if l.startswith(("-", "*", "β€’")) or re.match(r"^\d+[\.\)]", l) or len(l) > 50:
bullets.append(l.lstrip("-*β€’ ").strip())
if len(bullets) >= n:
break
if not bullets:
parts = [p.strip() for p in text.split("\n\n") if p.strip()]
bullets = parts[:n]
return bullets[:n]
def _hashtags(csv_tags: str) -> str:
tags = [t.strip().replace("#", "") for t in (csv_tags or "").split(",") if t.strip()]
return "" if not tags else " " + " ".join(f"#{t}" for t in tags)
def _truncate_chars(s: str, max_chars: int) -> str:
return s if len(s) <= max_chars else s[:max_chars - 1] + "…"
# ----------------------------
# Lightweight templates (no extra LLM call)
# ----------------------------
def tpl_linkedin(strategy, brand, audience, objective, hashtags, max_words=180):
hook = f"{brand}: a sharper path to {objective} for {audience}."
pts = "\n".join([f"- {p}" for p in _first_n_points(strategy, 5)])
body = f"""**{hook}**
**Key insights**
{pts}
**Next 90 days**
- Prove the positioning with fast tests
- Double down on channels with strongest signals
- Track leading KPIs weekly
CTA: Comment β€œPLAYBOOK” if you want the GTM outline.{_hashtags(hashtags)}
"""
words = body.split()
return (" ".join(words[:max_words]) + "…") if len(words) > max_words else body
def tpl_tweet(strategy, brand, audience, objective, hashtags, max_chars=270):
points = _first_n_points(strategy, 3)
msg = f"{brand} β†’ {objective} for {audience}: " + " | ".join(_truncate_chars(p, 80) for p in points)
return _truncate_chars(msg + _hashtags(hashtags), max_chars)
def tpl_article(strategy, brand, audience, objective, hashtags, max_words=800):
intro = (f"{brand} is targeting {audience} to achieve {objective}. "
"Here’s the distilled market analysis, GTM strategy, and a 90-day plan.")
text = f"""# {brand}: GTM Playbook for {audience}
## Why this matters
{intro}
## Strategy in brief
- Positioning & messaging pillars
- Priority segments & channel mix
- Offers/CTAs, KPI tree
- 90-day roadmap & budget split
## Research & Strategy Notes
{strategy}
## What to do next
1) Validate 1–2 offers with tight ICP cohorts
2) Launch a 2-channel test with weekly KPI reviews
3) Scale what converts, archive what doesn’t
*Updated: {datetime.utcnow().strftime("%Y-%m-%d")}*{_hashtags(hashtags)}
"""
words = text.split()
return (" ".join(words[:max_words]) + "…") if len(words) > max_words else text
# ----------------------------
# Optional LLM copywriter
# ----------------------------
def llm_copywriter(strategy_text, brand, audience, objective, tone, platform,
hashtags, li_words, tweet_chars, article_words):
limits = {
"LinkedIn": f"β‰ˆ {li_words} words",
"X (Twitter)": f"≀ {tweet_chars} chars",
"Article": f"β‰ˆ {article_words} words",
}
req = f"""Create a {platform} post from the GTM strategy.
Brand: {brand}
Audience: {audience}
Objective: {objective}
Tone: {tone}
Hashtags: {hashtags or '(none)'}
Length limit: {limits.get(platform, 'keep concise')}
Requirements:
- Strong first-line hook
- 1–3 concrete insights from the strategy (no clichΓ©s)
- Clear CTA
- Respect platform style and length
--- STRATEGY ---
{strategy_text}
"""
task = Task(description=req, expected_output=f"{platform} copy ready to publish.", agent=social_copywriter)
crew = Crew(agents=[social_copywriter], tasks=[task], verbose=False)
return crew.kickoff()
# ----------------------------
# Generation wrapper
# ----------------------------
PLATFORMS = ["LinkedIn", "X (Twitter)", "Article"] # Reduced for better UX
def generate(product_brand, target_audience, objective,
platform, tone, hashtags, use_llm,
li_max_words, tweet_max_chars, article_max_words):
if not product_brand or not target_audience or not objective:
return "Please fill Brand, Audience, and Objective.", "", None
# 1) Run the main Crew once
strategy = run_marketing_crew(product_brand, target_audience, objective)
# 2) Copy route
if use_llm:
social = llm_copywriter(strategy, product_brand, target_audience, objective,
tone, platform, hashtags,
li_max_words, tweet_max_chars, article_max_words)
else:
if platform == "LinkedIn":
social = tpl_linkedin(strategy, product_brand, target_audience, objective, hashtags, li_max_words)
fname = f"linkedin_{re.sub(r'\\W+','_',product_brand.lower())}.md"
elif platform == "X (Twitter)":
social = tpl_tweet(strategy, product_brand, target_audience, objective, hashtags, tweet_max_chars)
fname = f"tweet_{re.sub(r'\\W+','_',product_brand.lower())}.txt"
else: # Article
social = tpl_article(strategy, product_brand, target_audience, objective, hashtags, article_max_words)
fname = f"article_{re.sub(r'\\W+','_',product_brand.lower())}.md"
out_path = Path(fname).resolve()
out_path.write_text(social, encoding="utf-8")
return strategy, social, str(out_path)
# Save LLM result with reasonable extension
name_map = {"LinkedIn": "linkedin", "X (Twitter)": "tweet", "Article": "article"}
fname = f"{name_map.get(platform,'post')}_{re.sub(r'\\W+','_',product_brand.lower())}.md"
out_path = Path(fname).resolve()
out_path.write_text(social, encoding="utf-8")
return strategy, social, str(out_path)
# ----------------------------
# Theming & Layout (2 columns with header)
# ----------------------------
theme = gr.themes.Soft(primary_hue="indigo", neutral_hue="slate")
CUSTOM_CSS = """
#header {display:flex; align-items:center; gap:16px; padding:10px 14px; border-radius:12px;
background: linear-gradient(90deg, #eef2ff, #f8fafc); border:1px solid #e5e7eb;}
#header img {width:44px; height:44px; object-fit:contain; border-radius:8px;}
#header .title {font-weight:700; font-size:18px; color:#111827;}
#header .subtitle {font-size:13px; color:#6b7280;}
.card {border:1px solid #e5e7eb; border-radius:12px; padding:12px; background:#ffffff;}
"""
with gr.Blocks(title="DDS Marketing Crew β†’ Social Content", theme=theme, css=CUSTOM_CSS) as demo:
with gr.Row():
with gr.Column(scale=12):
with gr.Row(elem_id="header"):
gr.Image(value=LOGO_URL, show_label=False, interactive=False, height=48, width=48)
gr.HTML("""
<div>
<div class="title">Decoding Data Science β€” Marketing Strategy β†’ Social Generator</div>
<div class="subtitle">Run the Analyst β†’ Strategist β†’ Creator pipeline, then produce a platform-ready post.</div>
</div>
""")
with gr.Row():
# Left: Inputs
with gr.Column(scale=5):
with gr.Group(elem_classes="card"):
brand = gr.Textbox(label="Product/Brand", placeholder="e.g., DDS AI Residency", autofocus=True)
audience = gr.Textbox(label="Target Audience", placeholder="e.g., Data science beginners in MENA")
objective = gr.Textbox(label="Objective", placeholder="e.g., Drive applications for Cohort 8")
with gr.Group(elem_classes="card"):
platform = gr.Dropdown(choices=PLATFORMS, value="LinkedIn", label="Platform")
tone = gr.Dropdown(choices=["Professional", "Friendly", "Bold", "Educational", "Conversational"],
value="Professional", label="Tone")
hashtags = gr.Textbox(label="Hashtags (comma separated)", placeholder="ai, generativeai, datascience")
use_llm = gr.Checkbox(value=False, label="Use LLM Copywriter (higher fidelity)")
with gr.Group(elem_classes="card"):
li_max_words = gr.Slider(100, 350, value=180, step=10, label="LinkedIn max words")
tweet_max_chars = gr.Slider(120, 280, value=270, step=5, label="X/Tweet max chars")
article_max_words = gr.Slider(400, 1200, value=800, step=50, label="Article max words")
run_btn = gr.Button("Generate Strategy β†’ Post", variant="primary")
# Right: Outputs
with gr.Column(scale=7):
with gr.Accordion("Strategy Output (from Crew)", open=True):
strategy_md = gr.Markdown(value="*(Will appear here after generation)*")
with gr.Group(elem_classes="card"):
gr.Markdown("**Platform Copy** (editable; use the copy icon)")
social_tb = gr.Textbox(lines=18, show_copy_button=True, label=None)
download_file = gr.File(label="Download", interactive=False)
run_btn.click(
fn=generate,
inputs=[brand, audience, objective, platform, tone, hashtags, use_llm,
li_max_words, tweet_max_chars, article_max_words],
outputs=[strategy_md, social_tb, download_file]
)
# Space-friendly launch
if __name__ == "__main__":
# Space runners set HOST/PORT; default to 0.0.0.0:7860 for local
host = os.getenv("HOST", "0.0.0.0")
port = int(os.getenv("PORT", "7860"))
demo.launch(server_name=host, server_port=port)