decodingdatascience commited on
Commit
c48511a
·
verified ·
1 Parent(s): 3ef2679

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +333 -0
app.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py — Hugging Face Spaces (Gradio) for DDS Strategy → Social Generator
2
+ # Two-column UI, DDS logo, reduced dropdown (LinkedIn / X / Article)
3
+ # Secrets: set OPENAI_API_KEY in Spaces -> Settings -> Secrets
4
+
5
+ import os
6
+ import re
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ import gradio as gr
11
+ from crewai import Agent, Task, Crew
12
+
13
+ # --- Optional: ensure key exists (CrewAI will pick it up) ---
14
+ # If the key is missing, we still allow template-mode to work.
15
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
16
+
17
+ # --- DDS logo (raw GitHub URL conversion) ---
18
+ def to_raw_github(url: str) -> str:
19
+ return url.replace("https://github.com/", "https://raw.githubusercontent.com/").replace("/blob/", "/")
20
+
21
+ LOGO_URL = to_raw_github("https://github.com/Decoding-Data-Science/airesidency/blob/main/dds_logo.jpg")
22
+
23
+ # ----------------------------
24
+ # Agents (same logic)
25
+ # ----------------------------
26
+ lead_market_analyst = Agent(
27
+ role="Lead Market Analyst",
28
+ goal="Deliver sharp, data-driven market insights for the brand/product.",
29
+ backstory=("Senior analyst skilled in competitor intelligence, audience segmentation, "
30
+ "channel dynamics, and market sizing. Outputs actionable insights."),
31
+ allow_delegation=False,
32
+ verbose=True,
33
+ )
34
+
35
+ chief_marketing_strategist = Agent(
36
+ role="Chief Marketing Strategist",
37
+ goal="Turn research into a focused, measurable go-to-market strategy.",
38
+ backstory=("Veteran strategist who crafts positioning, messaging pillars, channel mix, "
39
+ "and KPI frameworks; coordinates the team."),
40
+ allow_delegation=False,
41
+ verbose=True,
42
+ )
43
+
44
+ creative_content_creator = Agent(
45
+ role="Creative Content Creator",
46
+ goal="Transform the strategy into compelling campaign concepts and a content calendar.",
47
+ backstory=("Concept-to-copy creative converting strategy into campaign ideas, ad copy, "
48
+ "social posts, and long-form content."),
49
+ allow_delegation=False,
50
+ verbose=True,
51
+ )
52
+
53
+ # Optional: focused social copywriter (used only if toggled)
54
+ social_copywriter = Agent(
55
+ role="Social Copywriter",
56
+ goal="Turn strategy into platform-appropriate copy with strong hooks and clear CTAs.",
57
+ backstory=("Skilled at LinkedIn thought-leadership, X brevity, and long-form posts that convert."),
58
+ allow_delegation=False,
59
+ verbose=False,
60
+ )
61
+
62
+ # ----------------------------
63
+ # Core crew (unchanged)
64
+ # ----------------------------
65
+ def run_marketing_crew(product_brand: str, target_audience: str, objective: str) -> str:
66
+ topic = f"{product_brand} | Audience: {target_audience} | Objective: {objective}"
67
+
68
+ market_analysis_task = Task(
69
+ description=(
70
+ f"Conduct a concise market analysis for: {topic}. "
71
+ "Cover: (1) ICP & segments, (2) JTBD/pain points & objections, "
72
+ "(3) competitive landscape & whitespace, (4) demand signals & seasonality, "
73
+ "(5) channel dynamics (search/social/email/partners/events), "
74
+ "(6) keyword themes & content gaps, (7) risks/assumptions."
75
+ ),
76
+ expected_output=(
77
+ "A structured brief with bullet points for each section and a 5–8 point summary of "
78
+ "the most actionable insights."
79
+ ),
80
+ agent=lead_market_analyst
81
+ )
82
+
83
+ strategy_task = Task(
84
+ description=(
85
+ f"Using the Market Analysis brief, craft a go-to-market strategy for: {topic}. "
86
+ "Include: positioning statement, value prop, 3–5 messaging pillars, "
87
+ "priority segments, channel mix with rationale, offer/CTA ideas, "
88
+ "90-day roadmap (phases & owners), KPI tree (primary/leading indicators), "
89
+ "and a lightweight budget allocation (% by channel)."
90
+ ),
91
+ expected_output="A strategy document with the sections above plus a one-page executive summary.",
92
+ agent=chief_marketing_strategist,
93
+ context=[market_analysis_task]
94
+ )
95
+
96
+ creative_task = Task(
97
+ description=(
98
+ "Based on the Strategy, produce: (a) 3 campaign concepts (hook, angle, proof), "
99
+ "(b) ad copy variants (paid search & paid social), "
100
+ "(c) a 4-week content calendar (blog/LinkedIn/X/YouTube/Newsletter) with titles, "
101
+ "briefs, CTAs, intended KPIs, and (d) a landing-page wireframe outline "
102
+ "(hero, value blocks, social proof, FAQ)."
103
+ ),
104
+ expected_output="Campaign concepts + copy, a tabular content calendar, and a structured LP outline.",
105
+ agent=creative_content_creator,
106
+ context=[strategy_task]
107
+ )
108
+
109
+ crew = Crew(
110
+ agents=[lead_market_analyst, chief_marketing_strategist, creative_content_creator],
111
+ tasks=[market_analysis_task, strategy_task, creative_task],
112
+ verbose=True
113
+ )
114
+ return crew.kickoff() # full text output
115
+
116
+ # ----------------------------
117
+ # Helpers
118
+ # ----------------------------
119
+ def _first_n_points(text: str, n: int = 5):
120
+ lines = [l.strip() for l in text.splitlines() if l.strip()]
121
+ bullets = []
122
+ for l in lines:
123
+ if l.startswith(("-", "*", "•")) or re.match(r"^\d+[\.\)]", l) or len(l) > 50:
124
+ bullets.append(l.lstrip("-*• ").strip())
125
+ if len(bullets) >= n:
126
+ break
127
+ if not bullets:
128
+ parts = [p.strip() for p in text.split("\n\n") if p.strip()]
129
+ bullets = parts[:n]
130
+ return bullets[:n]
131
+
132
+ def _hashtags(csv_tags: str) -> str:
133
+ tags = [t.strip().replace("#", "") for t in (csv_tags or "").split(",") if t.strip()]
134
+ return "" if not tags else " " + " ".join(f"#{t}" for t in tags)
135
+
136
+ def _truncate_chars(s: str, max_chars: int) -> str:
137
+ return s if len(s) <= max_chars else s[:max_chars - 1] + "…"
138
+
139
+ # ----------------------------
140
+ # Templates (no extra LLM call)
141
+ # ----------------------------
142
+ def tpl_linkedin(strategy, brand, audience, objective, hashtags, max_words=180):
143
+ hook = f"{brand}: a sharper path to {objective} for {audience}."
144
+ pts = "\n".join([f"- {p}" for p in _first_n_points(strategy, 5)])
145
+ body = f"""**{hook}**
146
+
147
+ **Key insights**
148
+ {pts}
149
+
150
+ **Next 90 days**
151
+ - Prove the positioning with fast tests
152
+ - Double down on channels with strongest signals
153
+ - Track leading KPIs weekly
154
+
155
+ CTA: Comment “PLAYBOOK” if you want the GTM outline.{_hashtags(hashtags)}
156
+ """
157
+ words = body.split()
158
+ return (" ".join(words[:max_words]) + "…") if len(words) > max_words else body
159
+
160
+ def tpl_tweet(strategy, brand, audience, objective, hashtags, max_chars=270):
161
+ points = _first_n_points(strategy, 3)
162
+ msg = f"{brand} → {objective} for {audience}: " + " | ".join(_truncate_chars(p, 80) for p in points)
163
+ return _truncate_chars(msg + _hashtags(hashtags), max_chars)
164
+
165
+ def tpl_article(strategy, brand, audience, objective, hashtags, max_words=800):
166
+ intro = (f"{brand} is targeting {audience} to achieve {objective}. "
167
+ "Here’s the distilled market analysis, GTM strategy, and a 90-day plan.")
168
+ text = f"""# {brand}: GTM Playbook for {audience}
169
+
170
+ ## Why this matters
171
+ {intro}
172
+
173
+ ## Strategy in brief
174
+ - Positioning & messaging pillars
175
+ - Priority segments & channel mix
176
+ - Offers/CTAs, KPI tree
177
+ - 90-day roadmap & budget split
178
+
179
+ ## Research & Strategy Notes
180
+ {strategy}
181
+
182
+ ## What to do next
183
+ 1) Validate 1–2 offers with tight ICP cohorts
184
+ 2) Launch a 2-channel test with weekly KPI reviews
185
+ 3) Scale what converts, archive what doesn’t
186
+
187
+ *Updated: {datetime.utcnow().strftime("%Y-%m-%d")}*{_hashtags(hashtags)}
188
+ """
189
+ words = text.split()
190
+ return (" ".join(words[:max_words]) + "…") if len(words) > max_words else text
191
+
192
+ # ----------------------------
193
+ # Optional LLM copywriter (reuses Social Copywriter agent)
194
+ # ----------------------------
195
+ def llm_copywriter(strategy_text, brand, audience, objective, tone, platform,
196
+ hashtags, li_words, tweet_chars, article_words):
197
+ limits = {
198
+ "LinkedIn": f"≈ {li_words} words",
199
+ "X (Twitter)": f"≤ {tweet_chars} chars",
200
+ "Article": f"≈ {article_words} words",
201
+ }
202
+ req = f"""Create a {platform} post from the GTM strategy.
203
+
204
+ Brand: {brand}
205
+ Audience: {audience}
206
+ Objective: {objective}
207
+ Tone: {tone}
208
+ Hashtags: {hashtags or '(none)'}
209
+ Length limit: {limits.get(platform, 'keep concise')}
210
+
211
+ Requirements:
212
+ - Strong first-line hook
213
+ - 1–3 concrete insights from the strategy (no clichés)
214
+ - Clear CTA
215
+ - Respect platform style and length
216
+
217
+ --- STRATEGY ---
218
+ {strategy_text}
219
+ """
220
+ task = Task(description=req, expected_output=f"{platform} copy ready to publish.", agent=social_copywriter)
221
+ crew = Crew(agents=[social_copywriter], tasks=[task], verbose=False)
222
+ return crew.kickoff()
223
+
224
+ # ----------------------------
225
+ # Generation wrapper
226
+ # ----------------------------
227
+ PLATFORMS = ["LinkedIn", "X (Twitter)", "Article"] # Reduced for better UX
228
+
229
+ def generate(product_brand, target_audience, objective,
230
+ platform, tone, hashtags, use_llm,
231
+ li_max_words, tweet_max_chars, article_max_words):
232
+
233
+ if not product_brand or not target_audience or not objective:
234
+ return "Please fill Brand, Audience, and Objective.", "", None
235
+
236
+ # 1) Run the main Crew once
237
+ strategy = run_marketing_crew(product_brand, target_audience, objective)
238
+
239
+ # 2) Copy creation
240
+ if use_llm and OPENAI_API_KEY:
241
+ social = llm_copywriter(strategy, product_brand, target_audience, objective,
242
+ tone, platform, hashtags,
243
+ li_max_words, tweet_max_chars, article_max_words)
244
+ else:
245
+ if platform == "LinkedIn":
246
+ social = tpl_linkedin(strategy, product_brand, target_audience, objective, hashtags, li_max_words)
247
+ fname = f"linkedin_{re.sub(r'\\W+','_',product_brand.lower())}.md"
248
+ elif platform == "X (Twitter)":
249
+ social = tpl_tweet(strategy, product_brand, target_audience, objective, hashtags, tweet_max_chars)
250
+ fname = f"tweet_{re.sub(r'\\W+','_',product_brand.lower())}.txt"
251
+ else: # Article
252
+ social = tpl_article(strategy, product_brand, target_audience, objective, hashtags, article_max_words)
253
+ fname = f"article_{re.sub(r'\\W+','_',product_brand.lower())}.md"
254
+
255
+ out_path = Path(fname).resolve()
256
+ out_path.write_text(social, encoding="utf-8")
257
+ return strategy, social, str(out_path)
258
+
259
+ # Save LLM result
260
+ name_map = {"LinkedIn": "linkedin", "X (Twitter)": "tweet", "Article": "article"}
261
+ fname = f"{name_map.get(platform,'post')}_{re.sub(r'\\W+','_',product_brand.lower())}.md"
262
+ out_path = Path(fname).resolve()
263
+ out_path.write_text(social, encoding="utf-8")
264
+ return strategy, social, str(out_path)
265
+
266
+ # ----------------------------
267
+ # Theming & Layout (two columns + header)
268
+ # ----------------------------
269
+ theme = gr.themes.Soft(primary_hue="indigo", neutral_hue="slate")
270
+ CUSTOM_CSS = """
271
+ #header {display:flex; align-items:center; gap:16px; padding:10px 14px; border-radius:12px;
272
+ background: linear-gradient(90deg, #eef2ff, #f8fafc); border:1px solid #e5e7eb; margin-bottom:8px;}
273
+ #header img {width:44px; height:44px; object-fit:contain; border-radius:8px;}
274
+ #header .title {font-weight:700; font-size:18px; color:#111827;}
275
+ #header .subtitle {font-size:13px; color:#6b7280;}
276
+ .card {border:1px solid #e5e7eb; border-radius:12px; padding:12px; background:#ffffff;}
277
+ """
278
+
279
+ with gr.Blocks(title="DDS Marketing Crew → Social Content", theme=theme, css=CUSTOM_CSS) as demo:
280
+ with gr.Row():
281
+ with gr.Column(scale=12):
282
+ with gr.Row(elem_id="header"):
283
+ gr.Image(value=LOGO_URL, show_label=False, interactive=False, height=48, width=48)
284
+ gr.HTML("""
285
+ <div>
286
+ <div class="title">Decoding Data Science — Strategy → Social Generator</div>
287
+ <div class="subtitle">Run Analyst → Strategist → Creator, then produce LinkedIn / X / Article.</div>
288
+ </div>
289
+ """)
290
+
291
+ with gr.Row():
292
+ # Left: Inputs
293
+ with gr.Column(scale=5):
294
+ with gr.Group(elem_classes="card"):
295
+ brand = gr.Textbox(label="Product/Brand", placeholder="e.g., DDS AI Residency", autofocus=True)
296
+ audience = gr.Textbox(label="Target Audience", placeholder="e.g., Data science beginners in MENA")
297
+ objective = gr.Textbox(label="Objective", placeholder="e.g., Drive applications for Cohort 8")
298
+
299
+ with gr.Group(elem_classes="card"):
300
+ platform = gr.Dropdown(choices=PLATFORMS, value="LinkedIn", label="Platform")
301
+ tone = gr.Dropdown(choices=["Professional", "Friendly", "Bold", "Educational", "Conversational"],
302
+ value="Professional", label="Tone")
303
+ hashtags = gr.Textbox(label="Hashtags (comma separated)", placeholder="ai, generativeai, datascience")
304
+ use_llm = gr.Checkbox(value=False, label="Use LLM Copywriter (requires OPENAI_API_KEY)")
305
+
306
+ with gr.Group(elem_classes="card"):
307
+ li_max_words = gr.Slider(100, 350, value=180, step=10, label="LinkedIn max words")
308
+ tweet_max_chars = gr.Slider(120, 280, value=270, step=5, label="X/Tweet max chars")
309
+ article_max_words = gr.Slider(400, 1200, value=800, step=50, label="Article max words")
310
+
311
+ run_btn = gr.Button("Generate Strategy → Post", variant="primary")
312
+
313
+ # Right: Outputs
314
+ with gr.Column(scale=7):
315
+ with gr.Accordion("Strategy Output (from Crew)", open=True):
316
+ strategy_md = gr.Markdown(value="*(Will appear here after generation)*")
317
+
318
+ with gr.Group(elem_classes="card"):
319
+ gr.Markdown("**Platform Copy** (editable; use the copy icon)")
320
+ social_tb = gr.Textbox(lines=18, show_copy_button=True, label=None)
321
+
322
+ download_file = gr.File(label="Download", interactive=False)
323
+
324
+ run_btn.click(
325
+ fn=generate,
326
+ inputs=[brand, audience, objective, platform, tone, hashtags, use_llm,
327
+ li_max_words, tweet_max_chars, article_max_words],
328
+ outputs=[strategy_md, social_tb, download_file]
329
+ )
330
+
331
+ # HF Spaces will start Gradio automatically; leaving default launch is fine.
332
+ if __name__ == "__main__":
333
+ demo.launch()