# 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("""
Decoding Data Science — Marketing Strategy → Social Generator
Run the Analyst → Strategist → Creator pipeline, then produce a platform-ready post.
""") 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)