|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
|
|
|
def to_raw_github(url: str) -> str: |
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, |
|
|
) |
|
|
|
|
|
|
|
|
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, |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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] + "β¦" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PLATFORMS = ["LinkedIn", "X (Twitter)", "Article"] |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
strategy = run_marketing_crew(product_brand, target_audience, objective) |
|
|
|
|
|
|
|
|
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: |
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(): |
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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] |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
host = os.getenv("HOST", "0.0.0.0") |
|
|
port = int(os.getenv("PORT", "7860")) |
|
|
demo.launch(server_name=host, server_port=port) |
|
|
|