Added growth finder
Browse files- app.py +33 -3
- app/ads1/growth_finder.py +80 -0
app.py
CHANGED
|
@@ -14,6 +14,8 @@ print("IMPORT 4 OK", flush=True)
|
|
| 14 |
from app.ads1.search_term_optimizer import run_search_term_optimizer
|
| 15 |
from app.ads1.keyword_inspector import run_keyword_inspector
|
| 16 |
from app.ads1.budget_optimizer import run_budget_optimizer
|
|
|
|
|
|
|
| 17 |
|
| 18 |
# ==================================================
|
| 19 |
# ROMER / ADVISOR DASHBOARD THEME
|
|
@@ -1009,6 +1011,9 @@ def campaign_selected(campaign_name, full_state):
|
|
| 1009 |
campaign_state = on_campaign_select(full_state, campaign_name)
|
| 1010 |
return campaign_state, build_campaign_kpi_html(campaign_state.get("campaign_df"))
|
| 1011 |
|
|
|
|
|
|
|
|
|
|
| 1012 |
|
| 1013 |
# ==================================================
|
| 1014 |
# ADS ANALYST
|
|
@@ -1064,6 +1069,23 @@ def run_search_term_optimizer_card(state):
|
|
| 1064 |
|
| 1065 |
except Exception as e:
|
| 1066 |
return f"Search term optimization failed: {e}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1067 |
|
| 1068 |
@spaces.GPU(duration=120)
|
| 1069 |
def run_keyword_inspector_card(state):
|
|
@@ -1137,9 +1159,11 @@ with gr.Blocks(fill_height=True, fill_width=True, css=CSS) as demo:
|
|
| 1137 |
value="Search Term Cleaner",
|
| 1138 |
elem_classes=["ai-button-card", "search-term-cleaner-card"],
|
| 1139 |
)
|
| 1140 |
-
gr.
|
| 1141 |
-
|
| 1142 |
-
|
|
|
|
|
|
|
| 1143 |
ads_output = gr.Markdown(
|
| 1144 |
value="Select a campaign and click Ads Analyst.",
|
| 1145 |
elem_id="ads-output",
|
|
@@ -1172,6 +1196,12 @@ with gr.Blocks(fill_height=True, fill_width=True, css=CSS) as demo:
|
|
| 1172 |
outputs=ads_output,
|
| 1173 |
)
|
| 1174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1175 |
keyword_inspector_card.click(
|
| 1176 |
fn=run_keyword_inspector_card,
|
| 1177 |
inputs=campaign_state,
|
|
|
|
| 14 |
from app.ads1.search_term_optimizer import run_search_term_optimizer
|
| 15 |
from app.ads1.keyword_inspector import run_keyword_inspector
|
| 16 |
from app.ads1.budget_optimizer import run_budget_optimizer
|
| 17 |
+
from app.ads1.growth_finder import run_growth_finder
|
| 18 |
+
from app.ads1.campaign_doctor import run_campaign_doctor
|
| 19 |
|
| 20 |
# ==================================================
|
| 21 |
# ROMER / ADVISOR DASHBOARD THEME
|
|
|
|
| 1011 |
campaign_state = on_campaign_select(full_state, campaign_name)
|
| 1012 |
return campaign_state, build_campaign_kpi_html(campaign_state.get("campaign_df"))
|
| 1013 |
|
| 1014 |
+
# def load_campaign_doctor(dfs):
|
| 1015 |
+
# result = run_campaign_doctor(dfs)
|
| 1016 |
+
# return update_right_panel(result)
|
| 1017 |
|
| 1018 |
# ==================================================
|
| 1019 |
# ADS ANALYST
|
|
|
|
| 1069 |
|
| 1070 |
except Exception as e:
|
| 1071 |
return f"Search term optimization failed: {e}"
|
| 1072 |
+
|
| 1073 |
+
@spaces.GPU(duration=120)
|
| 1074 |
+
def run_growth_finder_card(state):
|
| 1075 |
+
try:
|
| 1076 |
+
if not state:
|
| 1077 |
+
return "Select a campaign first."
|
| 1078 |
+
|
| 1079 |
+
dfs = state.get("full_dfs")
|
| 1080 |
+
campaign_name = state.get("campaign_name")
|
| 1081 |
+
|
| 1082 |
+
if dfs is None or campaign_name is None:
|
| 1083 |
+
return "Campaign state is not properly initialized."
|
| 1084 |
+
|
| 1085 |
+
return run_growth_finder(dfs, campaign_name=campaign_name)
|
| 1086 |
+
|
| 1087 |
+
except Exception as e:
|
| 1088 |
+
return f"Search term optimization failed: {e}"
|
| 1089 |
|
| 1090 |
@spaces.GPU(duration=120)
|
| 1091 |
def run_keyword_inspector_card(state):
|
|
|
|
| 1159 |
value="Search Term Cleaner",
|
| 1160 |
elem_classes=["ai-button-card", "search-term-cleaner-card"],
|
| 1161 |
)
|
| 1162 |
+
growth_finder_card = gr.Button(
|
| 1163 |
+
value="Growth Finder",
|
| 1164 |
+
elem_classes=["ai-button-card", "growth-finder-card"],
|
| 1165 |
+
)
|
| 1166 |
+
|
| 1167 |
ads_output = gr.Markdown(
|
| 1168 |
value="Select a campaign and click Ads Analyst.",
|
| 1169 |
elem_id="ads-output",
|
|
|
|
| 1196 |
outputs=ads_output,
|
| 1197 |
)
|
| 1198 |
|
| 1199 |
+
growth_finder_card.click(
|
| 1200 |
+
fn=run_growth_finder_card,
|
| 1201 |
+
inputs=campaign_state,
|
| 1202 |
+
outputs=ads_output,
|
| 1203 |
+
)
|
| 1204 |
+
|
| 1205 |
keyword_inspector_card.click(
|
| 1206 |
fn=run_keyword_inspector_card,
|
| 1207 |
inputs=campaign_state,
|
app/ads1/growth_finder.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from app.recs.generate import generate_explanation, is_bad_llm_output
|
| 4 |
+
|
| 5 |
+
def build_growth_finder_features(df: pd.DataFrame) -> pd.DataFrame:
|
| 6 |
+
df = df.copy()
|
| 7 |
+
|
| 8 |
+
df["cost"] = df["cost"].fillna(0)
|
| 9 |
+
df["clicks"] = df["clicks"].fillna(0)
|
| 10 |
+
df["impressions"] = df["impressions"].fillna(0)
|
| 11 |
+
df["conversions"] = df.get("conversions", 0).fillna(0)
|
| 12 |
+
|
| 13 |
+
# Core signals
|
| 14 |
+
df["ctr"] = (df["clicks"] / df["impressions"].replace(0, 1)) * 100
|
| 15 |
+
df["cvr"] = (df["conversions"] / df["clicks"].replace(0, 1)) * 100
|
| 16 |
+
df["cpa"] = df["cost"] / df["conversions"].replace(0, 1)
|
| 17 |
+
|
| 18 |
+
# Growth signal (IMPORTANT)
|
| 19 |
+
df["efficiency_score"] = df["conversions"] / df["cost"].replace(0, 1)
|
| 20 |
+
|
| 21 |
+
return df
|
| 22 |
+
|
| 23 |
+
def build_growth_finder_context(dfs: dict, campaign_name: str | None = None):
|
| 24 |
+
df = dfs["keywords"].copy()
|
| 25 |
+
|
| 26 |
+
if campaign_name and "campaign_name" in df.columns:
|
| 27 |
+
df = df[df["campaign_name"] == campaign_name]
|
| 28 |
+
|
| 29 |
+
df = build_growth_finder_features(df)
|
| 30 |
+
|
| 31 |
+
# keep high-signal subset only (not rule-based, just signal control)
|
| 32 |
+
df = df.sort_values("cost", ascending=False).head(200)
|
| 33 |
+
|
| 34 |
+
return {
|
| 35 |
+
"campaign_name": campaign_name,
|
| 36 |
+
"keywords": df.to_dict("records")
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
def build_growth_finder_prompt(context: dict) -> str:
|
| 40 |
+
payload = json.dumps(context, indent=2, default=str)
|
| 41 |
+
name = context.get("campaign_name", "this account")
|
| 42 |
+
|
| 43 |
+
return (
|
| 44 |
+
f"Write 3 to 5 bullet points of actionable Google Ads scaling opportunities for {name}.\n"
|
| 45 |
+
"Use simple language. One insight per bullet. Start each line with '- '. No intro sentence.\n\n"
|
| 46 |
+
"Focus on:\n"
|
| 47 |
+
"- keywords/ad groups that can scale budget profitably\n"
|
| 48 |
+
"- patterns that show strong conversion efficiency\n"
|
| 49 |
+
"- underfunded high-performing segments\n"
|
| 50 |
+
"- expansion opportunities (similar keywords, match types, themes)\n"
|
| 51 |
+
"- signals of demand that are not fully exploited\n\n"
|
| 52 |
+
"- Only suggest scaling where performance supports it\n"
|
| 53 |
+
f"Data (JSON):\n{payload}"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
def run_growth_finder(dfs: dict, campaign_name: str | None = None) -> str:
|
| 57 |
+
print("\n🚀 [growth_finder] STARTED", flush=True)
|
| 58 |
+
|
| 59 |
+
if not dfs or "keywords" not in dfs:
|
| 60 |
+
return "⚠️ No keyword data available."
|
| 61 |
+
|
| 62 |
+
context = build_growth_finder_context(dfs, campaign_name)
|
| 63 |
+
|
| 64 |
+
print("🧠 [growth_finder] context built", flush=True)
|
| 65 |
+
|
| 66 |
+
prompt = build_growth_finder_prompt(context)
|
| 67 |
+
|
| 68 |
+
print("✍️ [growth_finder] prompt built", flush=True)
|
| 69 |
+
|
| 70 |
+
result = generate_explanation(prompt)
|
| 71 |
+
|
| 72 |
+
if is_bad_llm_output(result):
|
| 73 |
+
print("⚠️ [growth_finder] fallback triggered", flush=True)
|
| 74 |
+
return (
|
| 75 |
+
"- Unable to generate scaling opportunities right now.\n"
|
| 76 |
+
"- Try again or check data quality."
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
print("📤 [growth_finder] result received", flush=True)
|
| 80 |
+
return result
|