ps1811 commited on
Commit
db82ded
·
1 Parent(s): a5ca0ad

Budget optimizer card added

Browse files
Files changed (2) hide show
  1. app.py +33 -1
  2. app/ads1/budget_optimizer.py +76 -84
app.py CHANGED
@@ -13,6 +13,7 @@ from app.ads1.ads_analyst import run_ads_analyst_card
13
  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
 
17
  # ==================================================
18
  # ROMER / ADVISOR DASHBOARD THEME
@@ -555,6 +556,11 @@ button.ads-analyst-card::before {
555
  content: "Campaign insights.";
556
  }
557
 
 
 
 
 
 
558
  .keyword-inspector-card button::before,
559
  button.keyword-inspector-card::before {
560
  content: "Winning versus wasting keywords.";
@@ -1025,6 +1031,23 @@ def run_ads_card(state):
1025
  except Exception as e:
1026
  return f"Analysis failed: {e}"
1027
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1028
  @spaces.GPU(duration=120)
1029
  def run_search_term_optimizer_card(state):
1030
  try:
@@ -1100,7 +1123,10 @@ with gr.Blocks(fill_height=True, fill_width=True, css=CSS) as demo:
1100
  value="Ads Analyst",
1101
  elem_classes=["ai-button-card", "ads-analyst-card"],
1102
  )
1103
- gr.HTML(ai_card("Budget Optimizer", "Where to adjust spend?"))
 
 
 
1104
  keyword_inspector_card = gr.Button(
1105
  value="Keyword Inspector",
1106
  elem_classes=["ai-button-card", "keyword-inspector-card"],
@@ -1134,6 +1160,12 @@ with gr.Blocks(fill_height=True, fill_width=True, css=CSS) as demo:
1134
  outputs=ads_output,
1135
  )
1136
 
 
 
 
 
 
 
1137
  search_term_cleaner_card.click(
1138
  fn=run_search_term_optimizer_card,
1139
  inputs=campaign_state,
 
13
  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
 
556
  content: "Campaign insights.";
557
  }
558
 
559
+ .budget-optimizer-card button::before,
560
+ button.budget-optimizer-card::before {
561
+ content: "Where to adjust spend?";
562
+ }
563
+
564
  .keyword-inspector-card button::before,
565
  button.keyword-inspector-card::before {
566
  content: "Winning versus wasting keywords.";
 
1031
  except Exception as e:
1032
  return f"Analysis failed: {e}"
1033
 
1034
+ @spaces.GPU(duration=120)
1035
+ def run_budget_card(state):
1036
+ try:
1037
+ if not state:
1038
+ return "Select a campaign first."
1039
+
1040
+ dfs = state.get("full_dfs")
1041
+ campaign_name = state.get("campaign_name")
1042
+
1043
+ if dfs is None or campaign_name is None:
1044
+ return "Campaign state is not properly initialized."
1045
+
1046
+ return run_budget_optimizer(dfs, campaign_name=campaign_name)
1047
+
1048
+ except Exception as e:
1049
+ return f"Analysis failed: {e}"
1050
+
1051
  @spaces.GPU(duration=120)
1052
  def run_search_term_optimizer_card(state):
1053
  try:
 
1123
  value="Ads Analyst",
1124
  elem_classes=["ai-button-card", "ads-analyst-card"],
1125
  )
1126
+ budget_optimizer_card = gr.Button(
1127
+ value="Budget Optimizer",
1128
+ elem_classes=["ai-button-card", "budget-optimizer-card"],
1129
+ )
1130
  keyword_inspector_card = gr.Button(
1131
  value="Keyword Inspector",
1132
  elem_classes=["ai-button-card", "keyword-inspector-card"],
 
1160
  outputs=ads_output,
1161
  )
1162
 
1163
+ budget_optimizer_card.click(
1164
+ fn=run_ads_card,
1165
+ inputs=campaign_state,
1166
+ outputs=ads_output,
1167
+ )
1168
+
1169
  search_term_cleaner_card.click(
1170
  fn=run_search_term_optimizer_card,
1171
  inputs=campaign_state,
app/ads1/budget_optimizer.py CHANGED
@@ -1,113 +1,105 @@
1
  import json
2
-
3
  from app.recs.generate import generate_explanation, is_bad_llm_output
4
 
 
 
5
 
6
- def build_campaign_summary(dfs: dict) -> dict:
7
- df = dfs["keywords"]
 
 
8
 
9
- total_cost = df["cost"].sum()
10
- total_conv = df["conversions"].sum()
 
 
11
 
12
- avg_cpl = total_cost / total_conv if total_conv > 0 else 0
 
13
 
14
- return {
15
- "total_spend": round(float(total_cost), 2),
16
- "total_conversions": int(total_conv),
17
- "avg_cpl": round(float(avg_cpl), 2),
18
- }
19
 
 
 
20
 
21
- def build_scale_candidates(dfs: dict):
22
- kw = dfs["keywords"].copy()
23
- if kw.empty:
24
- return []
25
 
26
- kw["cpl"] = kw["cost"] / kw["conversions"].replace(0, 1)
27
 
28
- account_avg = kw["cost"].sum() / max(kw["conversions"].sum(), 1)
 
29
 
30
- winners = kw[(kw["conversions"] > 0) & (kw["cpl"] < account_avg * 0.7)]
31
- winners = winners.sort_values("conversions", ascending=False)
 
 
32
 
33
- return winners.head(3)[["keyword", "ad_group_name", "cpl", "conversions"]].to_dict("records")
 
 
34
 
 
 
35
 
36
- def build_cut_candidates(dfs: dict):
37
- kw = dfs["keywords"].copy()
38
- if kw.empty:
39
- return []
40
 
41
- kw["cpl"] = kw["cost"] / kw["conversions"].replace(0, 1)
42
 
43
- account_avg = kw["cost"].sum() / max(kw["conversions"].sum(), 1)
44
 
45
- losers = kw[(kw["cost"] > 0) & ((kw["conversions"] == 0) | (kw["cpl"] > account_avg * 1.5))]
46
- losers = losers.sort_values("cost", ascending=False)
 
 
 
47
 
48
- return losers.head(3)[["keyword", "ad_group_name", "cost", "conversions", "cpl"]].to_dict("records")
 
 
 
 
49
 
 
 
50
 
51
- def _dfs_for_campaign(dfs: dict, campaign_name: str | None) -> dict:
52
- if not campaign_name:
53
- return dfs
54
- out = dict(dfs)
55
- if "keywords" in dfs and "campaign_name" in dfs["keywords"].columns:
56
- out["keywords"] = dfs["keywords"][dfs["keywords"]["campaign_name"] == campaign_name]
57
- return out
58
 
 
 
 
 
59
 
60
- def build_budget_optimizer_context(dfs: dict, campaign_name: str | None = None) -> dict:
61
- ctx = {
62
- "summary": build_campaign_summary(dfs),
63
- "scale_candidates": build_scale_candidates(dfs),
64
- "cut_candidates": build_cut_candidates(dfs),
65
- }
66
- if campaign_name:
67
- ctx["campaign_name"] = campaign_name
68
- return ctx
69
 
 
 
 
 
 
 
 
 
 
70
 
71
- def build_budget_optimizer_prompt(context: dict) -> str:
72
- payload = json.dumps(context, indent=2, default=str)
73
- name = context.get("campaign_name", "this campaign")
74
- return (
75
- f"Write 3 to 5 bullet points on where to increase or cut budget for {name}.\n"
76
- "Use simple business language. Start each line with '- '. No intro sentence.\n\n"
77
- f"Data (JSON):\n{payload}"
78
- )
79
-
80
-
81
- def rule_based_budget(context: dict) -> str:
82
- bullets: list[str] = []
83
- for row in context.get("scale_candidates", [])[:2]:
84
- bullets.append(
85
- f"- Increase budget on '{row['keyword']}' — {row['conversions']} conversions at CPL ${row['cpl']:.2f}."
86
- )
87
- for row in context.get("cut_candidates", [])[:2]:
88
- if row.get("conversions", 0) == 0:
89
- bullets.append(f"- Cut spend on '{row['keyword']}' — ${row['cost']:.2f} spent with no conversions.")
90
- else:
91
- bullets.append(f"- Reduce budget on '{row['keyword']}' — CPL ${row['cpl']:.2f} is above average.")
92
- if not bullets:
93
- bullets.append("- Review keyword-level spend and shift budget toward terms with the lowest CPL.")
94
- return "\n\n".join(bullets[:5])
95
-
96
-
97
- def run_budget_optimizer_card(dfs, campaign_name: str | None = None):
98
- scoped = _dfs_for_campaign(dfs, campaign_name)
99
- context = build_budget_optimizer_context(scoped, campaign_name)
100
  prompt = build_budget_optimizer_prompt(context)
101
 
102
- rec = {
103
- "campaign_id": "BUDGET_OPTIMIZER",
104
- "type": "budget_optimization",
105
- "action": "reallocate_budget",
106
- "reason": prompt,
107
- }
108
 
109
- result = generate_explanation(prompt, rec=rec)
110
  if is_bad_llm_output(result):
111
- print("⚠️ [budget_optimizer] LLM fallback — using rule-based budget tips", flush=True)
112
- result = rule_based_budget(context)
113
- return result
 
 
 
 
 
 
1
  import json
2
+ import pandas as pd
3
  from app.recs.generate import generate_explanation, is_bad_llm_output
4
 
5
+ def build_budget_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 efficiency signals
14
+ df["ctr"] = (df["clicks"] / df["impressions"].replace(0, 1)) * 100
15
+ df["cpa"] = df["cost"] / df["conversions"].replace(0, 1)
16
+ df["cpc"] = df["cost"] / df["clicks"].replace(0, 1)
17
 
18
+ # Budget efficiency proxy (VERY important for reasoning)
19
+ df["conv_per_cost"] = df["conversions"] / df["cost"].replace(0, 1)
20
 
21
+ return df
 
 
 
 
22
 
23
+ def build_budget_optimizer_context(dfs: dict, campaign_name: str | None = None):
24
+ df = dfs["campaigns"].copy()
25
 
26
+ if campaign_name and "name" in df.columns:
27
+ df = df[df["name"] == campaign_name]
 
 
28
 
29
+ df = build_budget_features(df)
30
 
31
+ # Keep top variance slice (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
+ "campaigns": df.to_dict("records")
37
+ }
38
 
39
+ def build_budget_optimizer_prompt(context: dict) -> str:
40
+ payload = json.dumps(context, indent=2, default=str)
41
+ name = context.get("campaign_name", "this account")
42
 
43
+ return f"""
44
+ You are a senior Google Ads budget optimization strategist.
45
 
46
+ Your job is to identify how to reallocate budget to improve overall performance.
 
 
 
47
 
48
+ Campaign scope: {name}
49
 
50
+ You must analyze the data and decide:
51
 
52
+ - Where money is being wasted
53
+ - Which campaigns deserve MORE budget
54
+ - Which campaigns should be reduced or paused
55
+ - Any inefficient spend patterns
56
+ - Any hidden high-efficiency opportunities
57
 
58
+ IMPORTANT:
59
+ - Do NOT rely on hard thresholds
60
+ - Do NOT assume rules like "CPA > X = bad"
61
+ - Think in relative performance vs distribution
62
+ - Focus on efficiency vs cost imbalance
63
 
64
+ OUTPUT FORMAT:
65
+ Return 3 to 5 bullet points.
66
 
67
+ Each bullet must:
68
+ - start with "- "
69
+ - include a clear recommendation
70
+ - include reasoning based on metrics
 
 
 
71
 
72
+ Example style:
73
+ - "Shift budget from X to Y because..."
74
+ - "Reduce spend on Z due to..."
75
+ - "Increase allocation to A since..."
76
 
77
+ DATA:
78
+ {payload}
79
+ """
 
 
 
 
 
 
80
 
81
+ def run_budget_optimizer(dfs: dict, campaign_name: str | None = None) -> str:
82
+ print("\n🚀 [budget_optimizer] STARTED", flush=True)
83
+
84
+ if not dfs or "campaigns" not in dfs:
85
+ return "⚠️ No campaign data available."
86
+
87
+ context = build_budget_optimizer_context(dfs, campaign_name)
88
+
89
+ print("🧠 [budget_optimizer] context built", flush=True)
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  prompt = build_budget_optimizer_prompt(context)
92
 
93
+ print("✍️ [budget_optimizer] prompt built", flush=True)
94
+
95
+ result = generate_explanation(prompt)
 
 
 
96
 
 
97
  if is_bad_llm_output(result):
98
+ print("⚠️ [budget_optimizer] fallback triggered", flush=True)
99
+ return (
100
+ "- Unable to generate budget recommendations right now.\n"
101
+ "- Try again or check campaign data quality."
102
+ )
103
+
104
+ print("📤 [budget_optimizer] result received", flush=True)
105
+ return result