ps1811 commited on
Commit
2976301
·
1 Parent(s): 919d91a

Prompts and card logics improved

Browse files
app/ads1/budget_optimizer.py CHANGED
@@ -36,16 +36,16 @@ def build_budget_optimizer_context(dfs: dict, campaign_name: str | None = None):
36
  total_conversions = dfs["campaigns"]["conversions"].fillna(0).sum()
37
  account_cpa = dfs["campaigns"]["cost"].fillna(0).sum() / total_conversions if total_conversions else 0
38
 
39
- def action_for(row):
40
  if row["conversions"] == 0:
41
- return "Reduce or cap budget until conversion tracking and intent quality are fixed."
42
  if account_cpa and row["cpa"] <= account_cpa * 0.8:
43
- return "Increase budget cautiously because CPA is better than the account average."
44
  if account_cpa and row["cpa"] >= account_cpa * 1.25:
45
- return "Reduce budget or tighten bids because CPA is worse than the account average."
46
- return "Hold budget and monitor because efficiency is near the account average."
47
 
48
- df["budget_action"] = df.apply(action_for, axis=1)
49
  df["segment"] = "Campaign"
50
  df = df.sort_values(["conv_per_cost", "conversions"], ascending=[False, False]).head(8)
51
 
@@ -64,7 +64,7 @@ def build_budget_optimizer_context(dfs: dict, campaign_name: str | None = None):
64
  kw = build_budget_features(kw)
65
  kw["name"] = "Keyword: " + kw["keyword"].astype(str)
66
  kw["segment"] = "Keyword"
67
- kw["budget_action"] = kw.apply(action_for, axis=1)
68
  kw = kw.sort_values(["conv_per_cost", "conversions", "cost"], ascending=[False, False, False]).head(6)
69
  kw_cols = [col for col in keep_cols if col in kw.columns]
70
  action_rows = pd.concat([action_rows, kw[kw_cols]], ignore_index=True)
@@ -81,10 +81,40 @@ def build_budget_optimizer_prompt(context: dict) -> str:
81
 
82
  return (
83
  f"Write 3 to 5 bullet points of actionable budget optimization insights for {name}.\n"
84
- "Use the budget_actions list only. Say where to increase, reduce, hold, or protect budget using cost, conversions, CPA, CPC, and conversion efficiency.\n"
85
- "Use simple language. One clear budget action per bullet. Start each line with '- '. No intro sentence.\n\n"
86
  f"Data (JSON):\n{payload}"
87
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
 
90
  def run_budget_optimizer(dfs: dict, campaign_name: str | None = None) -> str:
@@ -103,12 +133,9 @@ def run_budget_optimizer(dfs: dict, campaign_name: str | None = None) -> str:
103
 
104
  result = generate_explanation(prompt)
105
 
106
- if is_bad_llm_output(result):
107
  print("⚠️ [budget_optimizer] fallback triggered", flush=True)
108
- return (
109
- "- Unable to generate budget recommendations right now.\n"
110
- "- Try again or check campaign data quality."
111
- )
112
 
113
  print("📤 [budget_optimizer] result received", flush=True)
114
  return result
 
36
  total_conversions = dfs["campaigns"]["conversions"].fillna(0).sum()
37
  account_cpa = dfs["campaigns"]["cost"].fillna(0).sum() / total_conversions if total_conversions else 0
38
 
39
+ def action_type_for(row):
40
  if row["conversions"] == 0:
41
+ return "reduce"
42
  if account_cpa and row["cpa"] <= account_cpa * 0.8:
43
+ return "increase"
44
  if account_cpa and row["cpa"] >= account_cpa * 1.25:
45
+ return "reduce"
46
+ return "hold"
47
 
48
+ df["budget_action"] = df.apply(action_type_for, axis=1)
49
  df["segment"] = "Campaign"
50
  df = df.sort_values(["conv_per_cost", "conversions"], ascending=[False, False]).head(8)
51
 
 
64
  kw = build_budget_features(kw)
65
  kw["name"] = "Keyword: " + kw["keyword"].astype(str)
66
  kw["segment"] = "Keyword"
67
+ kw["budget_action"] = kw.apply(action_type_for, axis=1)
68
  kw = kw.sort_values(["conv_per_cost", "conversions", "cost"], ascending=[False, False, False]).head(6)
69
  kw_cols = [col for col in keep_cols if col in kw.columns]
70
  action_rows = pd.concat([action_rows, kw[kw_cols]], ignore_index=True)
 
81
 
82
  return (
83
  f"Write 3 to 5 bullet points of actionable budget optimization insights for {name}.\n"
84
+ "Use the budget_actions list only. Each bullet must mention the campaign or keyword name, the budget action, and the evidence.\n"
85
+ "Use simple language. One self-contained budget action per bullet. Start each line with '- '. No intro sentence. Do not quote JSON values by themselves.\n\n"
86
  f"Data (JSON):\n{payload}"
87
  )
88
+
89
+
90
+ def rule_based_budget_actions(context: dict) -> str:
91
+ rows = context.get("budget_actions", [])
92
+ if not rows:
93
+ return "- Hold budget for this campaign because no usable budget rows were found; verify campaign and keyword data before changing spend."
94
+
95
+ bullets = []
96
+ for row in rows[:5]:
97
+ name = row.get("name", "this segment")
98
+ action = row.get("budget_action", "hold")
99
+ cost = row.get("cost", 0)
100
+ conversions = row.get("conversions", 0)
101
+ cpa = row.get("cpa", 0)
102
+ cpc = row.get("cpc", 0)
103
+ conv_per_cost = row.get("conv_per_cost", 0)
104
+
105
+ if action == "increase":
106
+ bullets.append(
107
+ f"- Increase budget cautiously on {name} because it has {conversions} conversions at CPA {cpa:.2f}, CPC {cpc:.2f}, and conversion efficiency {conv_per_cost:.3f} on {cost:.2f} spend."
108
+ )
109
+ elif action == "reduce":
110
+ bullets.append(
111
+ f"- Reduce or cap budget on {name} because it has {conversions} conversions at CPA {cpa:.2f} after {cost:.2f} spend, making it a weaker use of budget."
112
+ )
113
+ else:
114
+ bullets.append(
115
+ f"- Hold budget on {name} because performance is near benchmark with {conversions} conversions, CPA {cpa:.2f}, and CPC {cpc:.2f}."
116
+ )
117
+ return "\n\n".join(bullets)
118
 
119
 
120
  def run_budget_optimizer(dfs: dict, campaign_name: str | None = None) -> str:
 
133
 
134
  result = generate_explanation(prompt)
135
 
136
+ if is_bad_llm_output(result) or not result.strip().startswith("-") or result.count('"') >= 4:
137
  print("⚠️ [budget_optimizer] fallback triggered", flush=True)
138
+ return rule_based_budget_actions(context)
 
 
 
139
 
140
  print("📤 [budget_optimizer] result received", flush=True)
141
  return result
app/ads1/growth_finder.py CHANGED
@@ -46,13 +46,7 @@ def build_growth_finder_context(dfs: dict, campaign_name: str | None = None):
46
  ascending=[False, False, False],
47
  ).head(8)
48
 
49
- df["growth_action"] = df.apply(
50
- lambda row: (
51
- f"Scale '{row['keyword']}' with a cautious bid or budget increase because it has "
52
- f"{int(row['conversions'])} conversions at CPA {row['cpa']:.2f}."
53
- ),
54
- axis=1,
55
- )
56
 
57
  keep_cols = [
58
  col
@@ -73,11 +67,34 @@ def build_growth_finder_prompt(context: dict) -> str:
73
  return (
74
  f"Write 3 to 5 bullet points of actionable growth opportunities for {name}.\n"
75
  "Use the growth_candidates list only. Suggest ways to scale winners, expand related intent, or increase budget on efficient areas.\n"
76
- "Do not list weak keywords. Do not diagnose poor performance. Every bullet must include a growth action.\n"
77
- "Use simple language. One growth opportunity per bullet. Start each line with '- '. No intro sentence.\n\n"
78
  f"Data (JSON):\n{payload}"
79
  )
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  def run_growth_finder(dfs: dict, campaign_name: str | None = None) -> str:
82
  print("\n🚀 [growth_finder] STARTED", flush=True)
83
 
@@ -86,11 +103,7 @@ def run_growth_finder(dfs: dict, campaign_name: str | None = None) -> str:
86
 
87
  context = build_growth_finder_context(dfs, campaign_name)
88
  if not context.get("growth_candidates"):
89
- account_cpa = context.get("account_average_cpa", 0)
90
- return (
91
- f"- No safe scale-up candidate found for {campaign_name} because its converting keywords are not efficient enough versus the account average CPA of {account_cpa}.\n"
92
- "- Improve growth readiness first by cutting weak intent, tightening match types, and waiting for lower CPA before increasing budget."
93
- )
94
 
95
  print("🧠 [growth_finder] context built", flush=True)
96
 
@@ -100,12 +113,9 @@ def run_growth_finder(dfs: dict, campaign_name: str | None = None) -> str:
100
 
101
  result = generate_explanation(prompt)
102
 
103
- if is_bad_llm_output(result):
104
  print("⚠️ [growth_finder] fallback triggered", flush=True)
105
- return (
106
- "- Unable to generate scaling opportunities right now.\n"
107
- "- Try again or check data quality."
108
- )
109
 
110
  print("📤 [growth_finder] result received", flush=True)
111
  return result
 
46
  ascending=[False, False, False],
47
  ).head(8)
48
 
49
+ df["growth_action"] = "scale"
 
 
 
 
 
 
50
 
51
  keep_cols = [
52
  col
 
67
  return (
68
  f"Write 3 to 5 bullet points of actionable growth opportunities for {name}.\n"
69
  "Use the growth_candidates list only. Suggest ways to scale winners, expand related intent, or increase budget on efficient areas.\n"
70
+ "Each bullet must mention the keyword, the growth action, and the evidence. Do not list weak keywords or diagnose poor performance.\n"
71
+ "Use simple language. One self-contained growth opportunity per bullet. Start each line with '- '. No intro sentence. Do not quote JSON values by themselves.\n\n"
72
  f"Data (JSON):\n{payload}"
73
  )
74
 
75
+
76
+ def rule_based_growth_actions(context: dict) -> str:
77
+ rows = context.get("growth_candidates", [])
78
+ if not rows:
79
+ account_cpa = context.get("account_average_cpa", 0)
80
+ campaign_name = context.get("campaign_name", "this campaign")
81
+ return (
82
+ f"- No safe scale-up candidate found for {campaign_name} because its converting keywords are not efficient enough versus the account average CPA of {account_cpa}.\n"
83
+ "- Improve growth readiness first by cutting weak intent, tightening match types, and waiting for lower CPA before increasing budget."
84
+ )
85
+
86
+ bullets = []
87
+ for row in rows[:5]:
88
+ keyword = row.get("keyword", "this keyword")
89
+ conversions = row.get("conversions", 0)
90
+ cpa = row.get("cpa", 0)
91
+ cvr = row.get("cvr", 0)
92
+ ctr = row.get("ctr", 0)
93
+ bullets.append(
94
+ f"- Scale '{keyword}' because it has {conversions} conversions at CPA {cpa:.2f}, CVR {cvr:.2f}%, and CTR {ctr:.2f}%; test a cautious bid or budget increase."
95
+ )
96
+ return "\n\n".join(bullets)
97
+
98
  def run_growth_finder(dfs: dict, campaign_name: str | None = None) -> str:
99
  print("\n🚀 [growth_finder] STARTED", flush=True)
100
 
 
103
 
104
  context = build_growth_finder_context(dfs, campaign_name)
105
  if not context.get("growth_candidates"):
106
+ return rule_based_growth_actions(context)
 
 
 
 
107
 
108
  print("🧠 [growth_finder] context built", flush=True)
109
 
 
113
 
114
  result = generate_explanation(prompt)
115
 
116
+ if is_bad_llm_output(result) or not result.strip().startswith("-") or result.count('"') >= 4:
117
  print("⚠️ [growth_finder] fallback triggered", flush=True)
118
+ return rule_based_growth_actions(context)
 
 
 
119
 
120
  print("📤 [growth_finder] result received", flush=True)
121
  return result
app/ads1/search_term_optimizer.py CHANGED
@@ -34,7 +34,9 @@ def prepare_context_df(df: pd.DataFrame, max_rows: int = 200) -> pd.DataFrame:
34
  Instead of semantic filtering, we just cap size for token control.
35
  Keeps high-variance distribution intact.
36
  """
37
- return df.sort_values("cost", ascending=False).head(max_rows)
 
 
38
 
39
  # -------------------------
40
  # Prompt (LLM owns all reasoning now)
@@ -65,8 +67,8 @@ def build_search_optimizer_prompt(context: dict) -> str:
65
 
66
  return (
67
  f"Write 3 to 5 bullet points of actionable search term cleanup insights for {name}.\n"
68
- "Identify terms to pause, add as negatives, add as keywords, scale, or investigate using cost, clicks, conversions, CPA, and CVR.\n"
69
- "Use simple language. One search term action per bullet. Start each line with '- '. No intro sentence.\n\n"
70
  f"Data (JSON):\n{payload}"
71
  )
72
 
@@ -82,13 +84,47 @@ def build_search_optimizer_context(dfs: dict, campaign_name: str | None = None):
82
 
83
  df = build_search_term_features(df)
84
  df = prepare_context_df(df)
 
 
 
 
 
 
 
 
 
 
85
 
86
  return {
87
  "campaign_name": campaign_name,
88
- "search_terms": df.to_dict("records") # FULL dataset context (bounded)
89
  }
90
 
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  # -------------------------
93
  # Runner
94
  # -------------------------
@@ -108,12 +144,9 @@ def run_search_term_optimizer(dfs: dict, campaign_name: str | None = None) -> st
108
 
109
  result = generate_explanation(prompt)
110
 
111
- if is_bad_llm_output(result):
112
  print("⚠️ [search_term_optimizer] LLM fallback triggered", flush=True)
113
- return (
114
- "- Unable to generate insights right now.\n"
115
- "- Try again or check data quality."
116
- )
117
 
118
  print("📤 [search_term_optimizer] result received", flush=True)
119
  return result
 
34
  Instead of semantic filtering, we just cap size for token control.
35
  Keeps high-variance distribution intact.
36
  """
37
+ waste = df[df["conversions"] == 0].sort_values("cost", ascending=False).head(10)
38
+ converters = df[df["conversions"] > 0].sort_values(["conversions", "cpa"], ascending=[False, True]).head(10)
39
+ return pd.concat([waste, converters], ignore_index=True).drop_duplicates(subset=["search_term"]).head(max_rows)
40
 
41
  # -------------------------
42
  # Prompt (LLM owns all reasoning now)
 
67
 
68
  return (
69
  f"Write 3 to 5 bullet points of actionable search term cleanup insights for {name}.\n"
70
+ "Use the search_terms list only. total_cost is total spend for that search term; cpc is cost per click.\n"
71
+ "Each bullet must mention the search term, the action, and the evidence. Use simple language. Start each line with '- '. No intro sentence.\n\n"
72
  f"Data (JSON):\n{payload}"
73
  )
74
 
 
84
 
85
  df = build_search_term_features(df)
86
  df = prepare_context_df(df)
87
+ df["action_type"] = df.apply(
88
+ lambda row: "add as keyword or scale" if row["conversions"] > 0 else "pause or add as negative",
89
+ axis=1,
90
+ )
91
+ df = df.rename(columns={"cost": "total_cost"})
92
+ keep_cols = [
93
+ col
94
+ for col in ["search_term", "action_type", "total_cost", "clicks", "impressions", "conversions", "ctr", "cvr", "cpc", "cpa"]
95
+ if col in df.columns
96
+ ]
97
 
98
  return {
99
  "campaign_name": campaign_name,
100
+ "search_terms": df[keep_cols].round(2).to_dict("records")
101
  }
102
 
103
 
104
+ def rule_based_search_actions(context: dict) -> str:
105
+ rows = context.get("search_terms", [])
106
+ if not rows:
107
+ return "- No search term cleanup action found because no usable search term rows were available."
108
+
109
+ bullets = []
110
+ for row in rows[:5]:
111
+ term = row.get("search_term", "this search term")
112
+ total_cost = row.get("total_cost", 0)
113
+ clicks = row.get("clicks", 0)
114
+ conversions = row.get("conversions", 0)
115
+ cpa = row.get("cpa", 0)
116
+ cvr = row.get("cvr", 0)
117
+ if conversions > 0:
118
+ bullets.append(
119
+ f"- Add or scale '{term}' because it produced {conversions} conversions from {clicks} clicks at CPA {cpa:.2f} and CVR {cvr:.2f}% on {total_cost:.2f} total spend."
120
+ )
121
+ else:
122
+ bullets.append(
123
+ f"- Pause or add '{term}' as a negative because it spent {total_cost:.2f} across {clicks} clicks with 0 conversions."
124
+ )
125
+ return "\n\n".join(bullets)
126
+
127
+
128
  # -------------------------
129
  # Runner
130
  # -------------------------
 
144
 
145
  result = generate_explanation(prompt)
146
 
147
+ if is_bad_llm_output(result) or not result.strip().startswith("-") or "cost is" in result.lower():
148
  print("⚠️ [search_term_optimizer] LLM fallback triggered", flush=True)
149
+ return rule_based_search_actions(context)
 
 
 
150
 
151
  print("📤 [search_term_optimizer] result received", flush=True)
152
  return result