Update app.py
Browse files
app.py
CHANGED
|
@@ -1,32 +1,23 @@
|
|
| 1 |
-
# app.py
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
import os
|
| 6 |
import re
|
| 7 |
from datetime import datetime
|
| 8 |
from pathlib import Path
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
from crewai import Agent, Task, Crew
|
| 12 |
|
| 13 |
-
# --- Optional: ensure key exists (CrewAI will pick it up) ---
|
| 14 |
-
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
| 15 |
|
| 16 |
-
# ---
|
| 17 |
def to_raw_github(url: str) -> str:
|
|
|
|
| 18 |
return url.replace("https://github.com/", "https://raw.githubusercontent.com/").replace("/blob/", "/")
|
| 19 |
|
| 20 |
LOGO_URL = to_raw_github("https://github.com/Decoding-Data-Science/airesidency/blob/main/dds_logo.jpg")
|
| 21 |
|
| 22 |
-
# --- Helper: safe filename slug ---
|
| 23 |
-
def slugify(s: str) -> str:
|
| 24 |
-
# replace non-word chars with underscores, collapse repeats, strip edges
|
| 25 |
-
slug = re.sub(r"\W+", "_", (s or "").lower())
|
| 26 |
-
return slug.strip("_") or "post"
|
| 27 |
-
|
| 28 |
# ----------------------------
|
| 29 |
-
# Agents
|
| 30 |
# ----------------------------
|
| 31 |
lead_market_analyst = Agent(
|
| 32 |
role="Lead Market Analyst",
|
|
@@ -55,7 +46,7 @@ creative_content_creator = Agent(
|
|
| 55 |
verbose=True,
|
| 56 |
)
|
| 57 |
|
| 58 |
-
# Optional: focused social copywriter (
|
| 59 |
social_copywriter = Agent(
|
| 60 |
role="Social Copywriter",
|
| 61 |
goal="Turn strategy into platform-appropriate copy with strong hooks and clear CTAs.",
|
|
@@ -65,7 +56,7 @@ social_copywriter = Agent(
|
|
| 65 |
)
|
| 66 |
|
| 67 |
# ----------------------------
|
| 68 |
-
# Core crew
|
| 69 |
# ----------------------------
|
| 70 |
def run_marketing_crew(product_brand: str, target_audience: str, objective: str) -> str:
|
| 71 |
topic = f"{product_brand} | Audience: {target_audience} | Objective: {objective}"
|
|
@@ -116,7 +107,7 @@ def run_marketing_crew(product_brand: str, target_audience: str, objective: str)
|
|
| 116 |
tasks=[market_analysis_task, strategy_task, creative_task],
|
| 117 |
verbose=True
|
| 118 |
)
|
| 119 |
-
return crew.kickoff()
|
| 120 |
|
| 121 |
# ----------------------------
|
| 122 |
# Helpers
|
|
@@ -142,7 +133,7 @@ def _truncate_chars(s: str, max_chars: int) -> str:
|
|
| 142 |
return s if len(s) <= max_chars else s[:max_chars - 1] + "β¦"
|
| 143 |
|
| 144 |
# ----------------------------
|
| 145 |
-
#
|
| 146 |
# ----------------------------
|
| 147 |
def tpl_linkedin(strategy, brand, audience, objective, hashtags, max_words=180):
|
| 148 |
hook = f"{brand}: a sharper path to {objective} for {audience}."
|
|
@@ -195,7 +186,7 @@ def tpl_article(strategy, brand, audience, objective, hashtags, max_words=800):
|
|
| 195 |
return (" ".join(words[:max_words]) + "β¦") if len(words) > max_words else text
|
| 196 |
|
| 197 |
# ----------------------------
|
| 198 |
-
# Optional LLM copywriter
|
| 199 |
# ----------------------------
|
| 200 |
def llm_copywriter(strategy_text, brand, audience, objective, tone, platform,
|
| 201 |
hashtags, li_words, tweet_chars, article_words):
|
|
@@ -241,42 +232,41 @@ def generate(product_brand, target_audience, objective,
|
|
| 241 |
# 1) Run the main Crew once
|
| 242 |
strategy = run_marketing_crew(product_brand, target_audience, objective)
|
| 243 |
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
# 2) Copy creation
|
| 247 |
-
if use_llm and OPENAI_API_KEY:
|
| 248 |
social = llm_copywriter(strategy, product_brand, target_audience, objective,
|
| 249 |
tone, platform, hashtags,
|
| 250 |
li_max_words, tweet_max_chars, article_max_words)
|
| 251 |
else:
|
| 252 |
if platform == "LinkedIn":
|
| 253 |
social = tpl_linkedin(strategy, product_brand, target_audience, objective, hashtags, li_max_words)
|
| 254 |
-
fname = f"linkedin_{
|
| 255 |
elif platform == "X (Twitter)":
|
| 256 |
social = tpl_tweet(strategy, product_brand, target_audience, objective, hashtags, tweet_max_chars)
|
| 257 |
-
fname = f"tweet_{
|
| 258 |
else: # Article
|
| 259 |
social = tpl_article(strategy, product_brand, target_audience, objective, hashtags, article_max_words)
|
| 260 |
-
fname = f"article_{
|
| 261 |
|
| 262 |
out_path = Path(fname).resolve()
|
| 263 |
out_path.write_text(social, encoding="utf-8")
|
| 264 |
return strategy, social, str(out_path)
|
| 265 |
|
| 266 |
-
# Save LLM result
|
| 267 |
name_map = {"LinkedIn": "linkedin", "X (Twitter)": "tweet", "Article": "article"}
|
| 268 |
-
fname = f"{name_map.get(platform,'post')}_{
|
| 269 |
out_path = Path(fname).resolve()
|
| 270 |
out_path.write_text(social, encoding="utf-8")
|
| 271 |
return strategy, social, str(out_path)
|
| 272 |
|
| 273 |
# ----------------------------
|
| 274 |
-
# Theming & Layout (
|
| 275 |
# ----------------------------
|
| 276 |
theme = gr.themes.Soft(primary_hue="indigo", neutral_hue="slate")
|
|
|
|
| 277 |
CUSTOM_CSS = """
|
| 278 |
#header {display:flex; align-items:center; gap:16px; padding:10px 14px; border-radius:12px;
|
| 279 |
-
background: linear-gradient(90deg, #eef2ff, #f8fafc); border:1px solid #e5e7eb;
|
| 280 |
#header img {width:44px; height:44px; object-fit:contain; border-radius:8px;}
|
| 281 |
#header .title {font-weight:700; font-size:18px; color:#111827;}
|
| 282 |
#header .subtitle {font-size:13px; color:#6b7280;}
|
|
@@ -290,8 +280,8 @@ with gr.Blocks(title="DDS Marketing Crew β Social Content", theme=theme, css=C
|
|
| 290 |
gr.Image(value=LOGO_URL, show_label=False, interactive=False, height=48, width=48)
|
| 291 |
gr.HTML("""
|
| 292 |
<div>
|
| 293 |
-
<div class="title">Decoding Data Science β Strategy β Social Generator</div>
|
| 294 |
-
<div class="subtitle">Run Analyst β Strategist β Creator, then produce
|
| 295 |
</div>
|
| 296 |
""")
|
| 297 |
|
|
@@ -308,7 +298,7 @@ with gr.Blocks(title="DDS Marketing Crew β Social Content", theme=theme, css=C
|
|
| 308 |
tone = gr.Dropdown(choices=["Professional", "Friendly", "Bold", "Educational", "Conversational"],
|
| 309 |
value="Professional", label="Tone")
|
| 310 |
hashtags = gr.Textbox(label="Hashtags (comma separated)", placeholder="ai, generativeai, datascience")
|
| 311 |
-
use_llm = gr.Checkbox(value=False, label="Use LLM Copywriter (
|
| 312 |
|
| 313 |
with gr.Group(elem_classes="card"):
|
| 314 |
li_max_words = gr.Slider(100, 350, value=180, step=10, label="LinkedIn max words")
|
|
@@ -335,6 +325,9 @@ with gr.Blocks(title="DDS Marketing Crew β Social Content", theme=theme, css=C
|
|
| 335 |
outputs=[strategy_md, social_tb, download_file]
|
| 336 |
)
|
| 337 |
|
| 338 |
-
#
|
| 339 |
if __name__ == "__main__":
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
from crewai import Agent, Task, Crew
|
| 3 |
+
import gradio as gr
|
|
|
|
|
|
|
| 4 |
import re
|
| 5 |
from datetime import datetime
|
| 6 |
from pathlib import Path
|
| 7 |
+
import os
|
| 8 |
|
| 9 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
|
|
|
| 10 |
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
# --- Logo: convert GitHub page URL to raw if needed ---
|
| 13 |
def to_raw_github(url: str) -> str:
|
| 14 |
+
# Accepts both blob and raw URLs; converts blob β raw
|
| 15 |
return url.replace("https://github.com/", "https://raw.githubusercontent.com/").replace("/blob/", "/")
|
| 16 |
|
| 17 |
LOGO_URL = to_raw_github("https://github.com/Decoding-Data-Science/airesidency/blob/main/dds_logo.jpg")
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# ----------------------------
|
| 20 |
+
# Agents
|
| 21 |
# ----------------------------
|
| 22 |
lead_market_analyst = Agent(
|
| 23 |
role="Lead Market Analyst",
|
|
|
|
| 46 |
verbose=True,
|
| 47 |
)
|
| 48 |
|
| 49 |
+
# Optional: focused social copywriter (only if toggled)
|
| 50 |
social_copywriter = Agent(
|
| 51 |
role="Social Copywriter",
|
| 52 |
goal="Turn strategy into platform-appropriate copy with strong hooks and clear CTAs.",
|
|
|
|
| 56 |
)
|
| 57 |
|
| 58 |
# ----------------------------
|
| 59 |
+
# Core crew
|
| 60 |
# ----------------------------
|
| 61 |
def run_marketing_crew(product_brand: str, target_audience: str, objective: str) -> str:
|
| 62 |
topic = f"{product_brand} | Audience: {target_audience} | Objective: {objective}"
|
|
|
|
| 107 |
tasks=[market_analysis_task, strategy_task, creative_task],
|
| 108 |
verbose=True
|
| 109 |
)
|
| 110 |
+
return crew.kickoff()
|
| 111 |
|
| 112 |
# ----------------------------
|
| 113 |
# Helpers
|
|
|
|
| 133 |
return s if len(s) <= max_chars else s[:max_chars - 1] + "β¦"
|
| 134 |
|
| 135 |
# ----------------------------
|
| 136 |
+
# Lightweight templates (no extra LLM call)
|
| 137 |
# ----------------------------
|
| 138 |
def tpl_linkedin(strategy, brand, audience, objective, hashtags, max_words=180):
|
| 139 |
hook = f"{brand}: a sharper path to {objective} for {audience}."
|
|
|
|
| 186 |
return (" ".join(words[:max_words]) + "β¦") if len(words) > max_words else text
|
| 187 |
|
| 188 |
# ----------------------------
|
| 189 |
+
# Optional LLM copywriter
|
| 190 |
# ----------------------------
|
| 191 |
def llm_copywriter(strategy_text, brand, audience, objective, tone, platform,
|
| 192 |
hashtags, li_words, tweet_chars, article_words):
|
|
|
|
| 232 |
# 1) Run the main Crew once
|
| 233 |
strategy = run_marketing_crew(product_brand, target_audience, objective)
|
| 234 |
|
| 235 |
+
# 2) Copy route
|
| 236 |
+
if use_llm:
|
|
|
|
|
|
|
| 237 |
social = llm_copywriter(strategy, product_brand, target_audience, objective,
|
| 238 |
tone, platform, hashtags,
|
| 239 |
li_max_words, tweet_max_chars, article_max_words)
|
| 240 |
else:
|
| 241 |
if platform == "LinkedIn":
|
| 242 |
social = tpl_linkedin(strategy, product_brand, target_audience, objective, hashtags, li_max_words)
|
| 243 |
+
fname = f"linkedin_{re.sub(r'\\W+','_',product_brand.lower())}.md"
|
| 244 |
elif platform == "X (Twitter)":
|
| 245 |
social = tpl_tweet(strategy, product_brand, target_audience, objective, hashtags, tweet_max_chars)
|
| 246 |
+
fname = f"tweet_{re.sub(r'\\W+','_',product_brand.lower())}.txt"
|
| 247 |
else: # Article
|
| 248 |
social = tpl_article(strategy, product_brand, target_audience, objective, hashtags, article_max_words)
|
| 249 |
+
fname = f"article_{re.sub(r'\\W+','_',product_brand.lower())}.md"
|
| 250 |
|
| 251 |
out_path = Path(fname).resolve()
|
| 252 |
out_path.write_text(social, encoding="utf-8")
|
| 253 |
return strategy, social, str(out_path)
|
| 254 |
|
| 255 |
+
# Save LLM result with reasonable extension
|
| 256 |
name_map = {"LinkedIn": "linkedin", "X (Twitter)": "tweet", "Article": "article"}
|
| 257 |
+
fname = f"{name_map.get(platform,'post')}_{re.sub(r'\\W+','_',product_brand.lower())}.md"
|
| 258 |
out_path = Path(fname).resolve()
|
| 259 |
out_path.write_text(social, encoding="utf-8")
|
| 260 |
return strategy, social, str(out_path)
|
| 261 |
|
| 262 |
# ----------------------------
|
| 263 |
+
# Theming & Layout (2 columns with header)
|
| 264 |
# ----------------------------
|
| 265 |
theme = gr.themes.Soft(primary_hue="indigo", neutral_hue="slate")
|
| 266 |
+
|
| 267 |
CUSTOM_CSS = """
|
| 268 |
#header {display:flex; align-items:center; gap:16px; padding:10px 14px; border-radius:12px;
|
| 269 |
+
background: linear-gradient(90deg, #eef2ff, #f8fafc); border:1px solid #e5e7eb;}
|
| 270 |
#header img {width:44px; height:44px; object-fit:contain; border-radius:8px;}
|
| 271 |
#header .title {font-weight:700; font-size:18px; color:#111827;}
|
| 272 |
#header .subtitle {font-size:13px; color:#6b7280;}
|
|
|
|
| 280 |
gr.Image(value=LOGO_URL, show_label=False, interactive=False, height=48, width=48)
|
| 281 |
gr.HTML("""
|
| 282 |
<div>
|
| 283 |
+
<div class="title">Decoding Data Science β Marketing Strategy β Social Generator</div>
|
| 284 |
+
<div class="subtitle">Run the Analyst β Strategist β Creator pipeline, then produce a platform-ready post.</div>
|
| 285 |
</div>
|
| 286 |
""")
|
| 287 |
|
|
|
|
| 298 |
tone = gr.Dropdown(choices=["Professional", "Friendly", "Bold", "Educational", "Conversational"],
|
| 299 |
value="Professional", label="Tone")
|
| 300 |
hashtags = gr.Textbox(label="Hashtags (comma separated)", placeholder="ai, generativeai, datascience")
|
| 301 |
+
use_llm = gr.Checkbox(value=False, label="Use LLM Copywriter (higher fidelity)")
|
| 302 |
|
| 303 |
with gr.Group(elem_classes="card"):
|
| 304 |
li_max_words = gr.Slider(100, 350, value=180, step=10, label="LinkedIn max words")
|
|
|
|
| 325 |
outputs=[strategy_md, social_tb, download_file]
|
| 326 |
)
|
| 327 |
|
| 328 |
+
# Space-friendly launch
|
| 329 |
if __name__ == "__main__":
|
| 330 |
+
# Space runners set HOST/PORT; default to 0.0.0.0:7860 for local
|
| 331 |
+
host = os.getenv("HOST", "0.0.0.0")
|
| 332 |
+
port = int(os.getenv("PORT", "7860"))
|
| 333 |
+
demo.launch(server_name=host, server_port=port)
|