ps1811 commited on
Commit
41cae8c
Β·
1 Parent(s): 8994207

Prompts updated

Browse files
app/ads1/ads_analyst.py CHANGED
@@ -134,9 +134,14 @@ def build_ads_analyst_prompt(context: dict) -> str:
134
  payload = json.dumps(context, indent=2, default=str)
135
  name = context.get("campaign_name", "this campaign")
136
  return (
137
- f"Write 3 to 5 bullet points of actionable Google Ads insights for {name}.\n"
138
- "Use simple language. One insight per bullet. Start each line with '- '. No intro sentence.\n\n"
139
- f"Data (JSON):\n{payload}"
 
 
 
 
 
140
  )
141
 
142
 
 
134
  payload = json.dumps(context, indent=2, default=str)
135
  name = context.get("campaign_name", "this campaign")
136
  return (
137
+ f"Write 3 to 5 executive Google Ads recommendations for {name}.\n"
138
+ "Return only markdown bullets. Start every line with '- '.\n"
139
+ "Each bullet must follow this exact pattern:\n"
140
+ "- <finding> β€” Evidence: <specific metric from data> β€” Action: <specific next step>\n"
141
+ "Do not restate the campaign name, raw data, prompt, task, or JSON keys.\n"
142
+ "Do not think aloud. Do not say what you can calculate. Do not invent causes.\n"
143
+ "If leads are 0, focus on conversion tracking, landing-page quality, and wasted spend risk.\n\n"
144
+ f"Data:\n{payload}"
145
  )
146
 
147
 
app/ads1/budget_optimizer.py CHANGED
@@ -28,12 +28,16 @@ def build_budget_optimizer_context(dfs: dict, campaign_name: str | None = None):
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:
@@ -64,6 +68,43 @@ def build_budget_optimizer_prompt(context: dict) -> str:
64
  )
65
 
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  def run_budget_optimizer(dfs: dict, campaign_name: str | None = None) -> str:
68
  print("\nπŸš€ [budget_optimizer] STARTED", flush=True)
69
 
@@ -82,10 +123,7 @@ def run_budget_optimizer(dfs: dict, campaign_name: str | None = None) -> str:
82
 
83
  if is_bad_llm_output(result):
84
  print("⚠️ [budget_optimizer] fallback triggered", flush=True)
85
- return (
86
- "- Unable to generate budget recommendations right now.\n"
87
- "- Try again or check campaign data quality."
88
- )
89
 
90
  print("πŸ“€ [budget_optimizer] result received", flush=True)
91
- return result
 
28
 
29
  df = build_budget_features(df)
30
 
31
+ keep_cols = [
32
+ col
33
+ for col in ["name", "cost", "clicks", "impressions", "conversions", "ctr", "cpa", "cpc", "conv_per_cost"]
34
+ if col in df.columns
35
+ ]
36
+ df = df.sort_values(["conversions", "conv_per_cost", "cost"], ascending=[False, False, False]).head(15)
37
 
38
  return {
39
  "campaign_name": campaign_name,
40
+ "campaigns": df[keep_cols].round(2).to_dict("records")
41
  }
42
 
43
  def build_budget_optimizer_prompt(context: dict) -> str:
 
68
  )
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 account")
74
+
75
+ return (
76
+ f"Write 3 to 5 budget allocation recommendations for {name}.\n"
77
+ "Return only markdown bullets. Start every line with '- '.\n"
78
+ "Each bullet must follow this exact pattern:\n"
79
+ "- <budget action> β€” Evidence: <cost, conversions, CPA/CPC/CTR from data> β€” Expected impact: <short outcome>\n"
80
+ "Use only the supplied campaign metrics. Do not repeat the prompt or data. Do not invent missing campaigns.\n"
81
+ "If there is only one campaign, recommend within-campaign caution instead of cross-campaign reallocation.\n\n"
82
+ f"Data:\n{payload}"
83
+ )
84
+
85
+
86
+ def rule_based_budget_actions(context: dict) -> str:
87
+ rows = context.get("campaigns", [])
88
+ if not rows:
89
+ return "- Hold budget β€” Evidence: no campaign rows available β€” Expected impact: prevents blind budget changes until data sync is fixed."
90
+
91
+ bullets = []
92
+ for row in rows[:5]:
93
+ name = row.get("name") or context.get("campaign_name") or "Selected campaign"
94
+ cost = row.get("cost", 0)
95
+ conversions = row.get("conversions", 0)
96
+ cpa = row.get("cpa", 0)
97
+ if conversions > 0:
98
+ bullets.append(
99
+ f"- Protect budget on {name} β€” Evidence: ${cost:.2f} cost, {conversions} conversions, ${cpa:.2f} CPA β€” Expected impact: keeps spend on proven demand."
100
+ )
101
+ else:
102
+ bullets.append(
103
+ f"- Cap budget on {name} β€” Evidence: ${cost:.2f} cost and 0 conversions β€” Expected impact: limits wasted spend while tracking or targeting is reviewed."
104
+ )
105
+ return "\n\n".join(bullets[:5])
106
+
107
+
108
  def run_budget_optimizer(dfs: dict, campaign_name: str | None = None) -> str:
109
  print("\nπŸš€ [budget_optimizer] STARTED", flush=True)
110
 
 
123
 
124
  if is_bad_llm_output(result):
125
  print("⚠️ [budget_optimizer] fallback triggered", flush=True)
126
+ return rule_based_budget_actions(context)
 
 
 
127
 
128
  print("πŸ“€ [budget_optimizer] result received", flush=True)
129
+ return result
app/ads1/growth_finder.py CHANGED
@@ -28,12 +28,19 @@ def build_growth_finder_context(dfs: dict, campaign_name: str | None = None):
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:
@@ -41,28 +48,33 @@ def build_growth_finder_prompt(context: dict) -> str:
41
  name = context.get("campaign_name", "this account")
42
 
43
  return (
44
- f"""
45
- You are a Google Ads growth strategist.
46
-
47
- TASK:
48
- Identify ONLY data-backed scaling opportunities.
49
-
50
- STRICT RULES:
51
- - If performance is weak, DO NOT suggest scaling.
52
- - Do not infer missing ad groups or structures.
53
- - No generic marketing theory.
54
-
55
- OUTPUT FORMAT:
56
- - Opportunity: <keyword / segment>
57
- Evidence: <CTR, conversions, CPA>
58
- Why it works: <short justification>
59
- Action: <scale suggestion (budget / bids / match type expansion)>
60
-
61
- DATA:
62
- {payload}
63
- """
64
  )
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  def run_growth_finder(dfs: dict, campaign_name: str | None = None) -> str:
67
  print("\nπŸš€ [growth_finder] STARTED", flush=True)
68
 
@@ -81,10 +93,7 @@ def run_growth_finder(dfs: dict, campaign_name: str | None = None) -> str:
81
 
82
  if is_bad_llm_output(result):
83
  print("⚠️ [growth_finder] fallback triggered", flush=True)
84
- return (
85
- "- Unable to generate scaling opportunities right now.\n"
86
- "- Try again or check data quality."
87
- )
88
 
89
  print("πŸ“€ [growth_finder] result received", flush=True)
90
- return result
 
28
 
29
  df = build_growth_finder_features(df)
30
 
31
+ df = df[df["conversions"] > 0].sort_values(
32
+ ["efficiency_score", "conversions", "ctr"],
33
+ ascending=[False, False, False],
34
+ ).head(20)
35
+ keep_cols = [
36
+ col
37
+ for col in ["keyword", "cost", "clicks", "impressions", "conversions", "ctr", "cvr", "cpa", "efficiency_score"]
38
+ if col in df.columns
39
+ ]
40
 
41
  return {
42
  "campaign_name": campaign_name,
43
+ "keywords": df[keep_cols].round(2).to_dict("records")
44
  }
45
 
46
  def build_growth_finder_prompt(context: dict) -> str:
 
48
  name = context.get("campaign_name", "this account")
49
 
50
  return (
51
+ f"Write 3 to 5 data-backed scaling opportunities for {name}.\n"
52
+ "Return only markdown bullets. Start every line with '- '.\n"
53
+ "Each bullet must follow this exact pattern:\n"
54
+ "- Scale: '<keyword>' β€” Evidence: <conversions, CPA, CTR/CVR from data> β€” Action: <specific budget, bid, or match-type expansion>\n"
55
+ "Only recommend scaling when conversions are greater than 0. If no scalable rows exist, say '- No scaling candidate β€” Evidence: no converting keyword rows β€” Action: fix tracking or demand quality first.'\n"
56
+ "Use only supplied metrics. Do not infer ad groups. Do not repeat the prompt. Do not think aloud.\n\n"
57
+ f"Data:\n{payload}"
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  )
59
 
60
+
61
+ def rule_based_growth_actions(context: dict) -> str:
62
+ rows = context.get("keywords", [])
63
+ if not rows:
64
+ return "- No scaling candidate β€” Evidence: no converting keyword rows β€” Action: fix tracking or demand quality before increasing budget."
65
+
66
+ bullets = []
67
+ for row in rows[:5]:
68
+ keyword = row.get("keyword", "Unknown keyword")
69
+ conversions = row.get("conversions", 0)
70
+ cpa = row.get("cpa", 0)
71
+ ctr = row.get("ctr", 0)
72
+ bullets.append(
73
+ f"- Scale: '{keyword}' β€” Evidence: {conversions} conversions, ${cpa:.2f} CPA, {ctr:.2f}% CTR β€” Action: test a small bid or budget increase while monitoring CPA."
74
+ )
75
+ return "\n\n".join(bullets)
76
+
77
+
78
  def run_growth_finder(dfs: dict, campaign_name: str | None = None) -> str:
79
  print("\nπŸš€ [growth_finder] STARTED", flush=True)
80
 
 
93
 
94
  if is_bad_llm_output(result):
95
  print("⚠️ [growth_finder] fallback triggered", flush=True)
96
+ return rule_based_growth_actions(context)
 
 
 
97
 
98
  print("πŸ“€ [growth_finder] result received", flush=True)
99
+ return result
app/ads1/keyword_inspector.py CHANGED
@@ -47,6 +47,43 @@ def build_keyword_prompt(context: dict) -> str:
47
  DATA:
48
  {payload}
49
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  # -------------------------
51
  # Main runner
52
  # -------------------------
@@ -63,9 +100,21 @@ def run_keyword_inspector(dfs: dict, campaign_name: str | None = None) -> str:
63
  if campaign_name and "campaign_name" in df.columns:
64
  df = df[df["campaign_name"] == campaign_name]
65
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  context = {
67
  "campaign_name": campaign_name,
68
- "keywords": df.to_dict("records") # FULL DATA given to LLM
69
  }
70
 
71
  print("🧠 [keyword_inspector] context built", flush=True)
@@ -77,10 +126,7 @@ def run_keyword_inspector(dfs: dict, campaign_name: str | None = None) -> str:
77
 
78
  if is_bad_llm_output(result):
79
  print("⚠️ [keyword_inspector] LLM fallback triggered", flush=True)
80
- return (
81
- "- Unable to generate LLM insights right now.\n"
82
- "- Check keyword data quality or retry."
83
- )
84
 
85
  print("πŸ“€ [keyword_inspector] result received", flush=True)
86
- return result
 
47
  DATA:
48
  {payload}
49
  """
50
+
51
+
52
+ def build_keyword_prompt(context: dict) -> str:
53
+ payload = json.dumps(context, indent=2, default=str)
54
+
55
+ return (
56
+ "Write 3 to 5 keyword recommendations for a preschool Google Ads campaign.\n"
57
+ "Return only markdown bullets. Start every line with '- '.\n"
58
+ "Each bullet must follow this exact pattern:\n"
59
+ "- <Label>: '<keyword>' β€” Evidence: <clicks, cost, conversions, CTR/CPA from data> β€” Action: <specific bid, pause, or scale action>\n"
60
+ "Allowed labels: Winning, Wasted Spend, Scale, Reduce, Investigate.\n"
61
+ "Use only supplied keyword metrics. Do not mention ad groups if ad group data is missing. Do not think aloud.\n\n"
62
+ f"Data:\n{payload}"
63
+ )
64
+
65
+
66
+ def rule_based_keyword_actions(context: dict) -> str:
67
+ bullets = []
68
+ for row in context.get("keywords", []):
69
+ keyword = row.get("keyword", "Unknown keyword")
70
+ cost = row.get("cost", 0)
71
+ clicks = row.get("clicks", 0)
72
+ conversions = row.get("conversions", 0)
73
+ cpa = row.get("cpa", 0)
74
+ if conversions > 0:
75
+ bullets.append(
76
+ f"- Winning: '{keyword}' β€” Evidence: {clicks} clicks, ${cost:.2f} cost, {conversions} conversions, ${cpa:.2f} CPA β€” Action: protect budget and test a modest bid increase."
77
+ )
78
+ elif cost > 0:
79
+ bullets.append(
80
+ f"- Wasted Spend: '{keyword}' β€” Evidence: {clicks} clicks, ${cost:.2f} cost, 0 conversions β€” Action: reduce bid or pause until intent is proven."
81
+ )
82
+ if len(bullets) >= 5:
83
+ break
84
+ return "\n\n".join(bullets) or "- Investigate: no keyword rows available β€” Evidence: no usable data β€” Action: verify keyword data sync."
85
+
86
+
87
  # -------------------------
88
  # Main runner
89
  # -------------------------
 
100
  if campaign_name and "campaign_name" in df.columns:
101
  df = df[df["campaign_name"] == campaign_name]
102
 
103
+ keep_cols = [
104
+ col
105
+ for col in ["keyword", "cost", "clicks", "impressions", "conversions", "ctr", "cpa"]
106
+ if col in df.columns
107
+ ]
108
+ winners = df[df["conversions"] > 0].sort_values(["conversions", "cpa"], ascending=[False, True]).head(8)
109
+ waste = df[df["conversions"] == 0].sort_values("cost", ascending=False).head(8)
110
+ df = pd.concat([winners, waste], ignore_index=True)
111
+ if "keyword" in df.columns:
112
+ df = df.drop_duplicates(subset=["keyword"])
113
+ df = df.head(20)
114
+
115
  context = {
116
  "campaign_name": campaign_name,
117
+ "keywords": df[keep_cols].round(2).to_dict("records")
118
  }
119
 
120
  print("🧠 [keyword_inspector] context built", flush=True)
 
126
 
127
  if is_bad_llm_output(result):
128
  print("⚠️ [keyword_inspector] LLM fallback triggered", flush=True)
129
+ return rule_based_keyword_actions(context)
 
 
 
130
 
131
  print("πŸ“€ [keyword_inspector] result received", flush=True)
132
+ return result
app/ads1/search_term_optimizer.py CHANGED
@@ -34,7 +34,13 @@ 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)
@@ -66,6 +72,22 @@ def build_search_optimizer_prompt(context: dict) -> str:
66
  """
67
  )
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  # -------------------------
70
  # Context builder (FULL DATA approach)
71
  # -------------------------
@@ -77,13 +99,39 @@ def build_search_optimizer_context(dfs: dict, campaign_name: str | None = None):
77
 
78
  df = build_search_term_features(df)
79
  df = prepare_context_df(df)
 
 
 
 
 
80
 
81
  return {
82
  "campaign_name": campaign_name,
83
- "search_terms": df.to_dict("records") # FULL dataset context (bounded)
84
  }
85
 
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  # -------------------------
88
  # Runner
89
  # -------------------------
@@ -105,10 +153,7 @@ def run_search_term_optimizer(dfs: dict, campaign_name: str | None = None) -> st
105
 
106
  if is_bad_llm_output(result):
107
  print("⚠️ [search_term_optimizer] LLM fallback triggered", flush=True)
108
- return (
109
- "- Unable to generate insights right now.\n"
110
- "- Try again or check data quality."
111
- )
112
 
113
  print("πŸ“€ [search_term_optimizer] result received", flush=True)
114
- return result
 
34
  Instead of semantic filtering, we just cap size for token control.
35
  Keeps high-variance distribution intact.
36
  """
37
+ converters = df[df["conversions"] > 0].sort_values(["conversions", "cpa"], ascending=[False, True]).head(12)
38
+ waste = df[df["conversions"] == 0].sort_values("cost", ascending=False).head(12)
39
+ high_intent = df[(df["conversions"] > 0) & (df["cvr"] > 0)].sort_values("cvr", ascending=False).head(8)
40
+ out = pd.concat([converters, waste, high_intent], ignore_index=True)
41
+ if "search_term" in out.columns:
42
+ out = out.drop_duplicates(subset=["search_term"])
43
+ return out.head(max_rows)
44
 
45
  # -------------------------
46
  # Prompt (LLM owns all reasoning now)
 
72
  """
73
  )
74
 
75
+
76
+ def build_search_optimizer_prompt(context: dict) -> str:
77
+ payload = json.dumps(context, indent=2, default=str)
78
+ name = context.get("campaign_name", "this campaign")
79
+
80
+ return (
81
+ f"Write 3 to 5 search term optimization actions for {name}.\n"
82
+ "Return only markdown bullets. Start every line with '- '.\n"
83
+ "Each bullet must follow this exact pattern:\n"
84
+ "- <Category>: '<search term>' β€” Evidence: <cost, clicks, conversions, CPA/CVR from data> β€” Action: <pause, reduce bid, add as keyword, or add as negative>\n"
85
+ "Allowed categories: Wasted Spend, High Intent, Scale, Negative Keyword Candidate, Investigate.\n"
86
+ "Use only supplied search terms. Do not explain calculations. Do not repeat the prompt. Do not think aloud.\n\n"
87
+ f"Data:\n{payload}"
88
+ )
89
+
90
+
91
  # -------------------------
92
  # Context builder (FULL DATA approach)
93
  # -------------------------
 
99
 
100
  df = build_search_term_features(df)
101
  df = prepare_context_df(df)
102
+ keep_cols = [
103
+ col
104
+ for col in ["search_term", "cost", "clicks", "impressions", "conversions", "ctr", "cvr", "cpc", "cpa"]
105
+ if col in df.columns
106
+ ]
107
 
108
  return {
109
  "campaign_name": campaign_name,
110
+ "search_terms": df[keep_cols].round(2).to_dict("records")
111
  }
112
 
113
 
114
+ def rule_based_search_actions(context: dict) -> str:
115
+ bullets = []
116
+ for row in context.get("search_terms", []):
117
+ term = row.get("search_term", "Unknown term")
118
+ cost = row.get("cost", 0)
119
+ clicks = row.get("clicks", 0)
120
+ conversions = row.get("conversions", 0)
121
+ cvr = row.get("cvr", 0)
122
+ if conversions > 0:
123
+ bullets.append(
124
+ f"- High Intent: '{term}' β€” Evidence: {clicks} clicks, ${cost:.2f} cost, {conversions} conversions, {cvr:.2f}% CVR β€” Action: add as keyword and test higher bid."
125
+ )
126
+ elif cost > 0:
127
+ bullets.append(
128
+ f"- Wasted Spend: '{term}' β€” Evidence: {clicks} clicks, ${cost:.2f} cost, 0 conversions β€” Action: reduce bid or add as negative if intent is poor."
129
+ )
130
+ if len(bullets) >= 5:
131
+ break
132
+ return "\n\n".join(bullets) or "- Investigate: no search terms available β€” Evidence: no usable rows β€” Action: verify search term data sync."
133
+
134
+
135
  # -------------------------
136
  # Runner
137
  # -------------------------
 
153
 
154
  if is_bad_llm_output(result):
155
  print("⚠️ [search_term_optimizer] LLM fallback triggered", flush=True)
156
+ return rule_based_search_actions(context)
 
 
 
157
 
158
  print("πŸ“€ [search_term_optimizer] result received", flush=True)
159
+ return result
app/recs/generate.py CHANGED
@@ -18,9 +18,10 @@ _infer_lock = threading.Lock()
18
 
19
  _SYSTEM = (
20
  "You are a Google Ads analyst. "
21
- "Reply with 3 to 5 markdown bullet points only. "
22
- "Each bullet must be one short, actionable insight about the campaign data. "
23
- "No introduction, no numbered lists, no step-by-step reasoning."
 
24
  )
25
 
26
 
@@ -45,6 +46,20 @@ def _looks_like_garbage(text: str) -> bool:
45
  lower = text.lower()
46
  if "return only" in lower or "no reasoning" in lower or "no explanation" in lower:
47
  return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  if "google ads analyst" in lower and text.count("-") < 2:
49
  return True
50
  if "ads performance analyst" in lower and text.count("-") < 2:
@@ -107,7 +122,7 @@ def _message_text(message: dict) -> str:
107
 
108
  def _infer(llm, messages: list[dict[str, str]]) -> str:
109
  max_tokens = int(os.getenv("LLAMA_MAX_TOKENS", "384"))
110
- temperature = float(os.getenv("LLAMA_TEMPERATURE", "0.35"))
111
 
112
  try:
113
  out = llm.create_chat_completion(
 
18
 
19
  _SYSTEM = (
20
  "You are a Google Ads analyst. "
21
+ "Reply with markdown bullets only. "
22
+ "Each bullet must be one short, data-backed recommendation. "
23
+ "Do not repeat the prompt, do not explain your reasoning process, and do not invent missing fields. "
24
+ "Use only the evidence supplied by the user."
25
  )
26
 
27
 
 
46
  lower = text.lower()
47
  if "return only" in lower or "no reasoning" in lower or "no explanation" in lower:
48
  return True
49
+ echo_markers = [
50
+ "task:",
51
+ "strict rules:",
52
+ "output format:",
53
+ "focus on:",
54
+ "data:",
55
+ "we can calculate",
56
+ "i assume",
57
+ "actually,",
58
+ "however, the data",
59
+ "let's",
60
+ ]
61
+ if any(marker in lower for marker in echo_markers):
62
+ return True
63
  if "google ads analyst" in lower and text.count("-") < 2:
64
  return True
65
  if "ads performance analyst" in lower and text.count("-") < 2:
 
122
 
123
  def _infer(llm, messages: list[dict[str, str]]) -> str:
124
  max_tokens = int(os.getenv("LLAMA_MAX_TOKENS", "384"))
125
+ temperature = float(os.getenv("LLAMA_TEMPERATURE", "0.15"))
126
 
127
  try:
128
  out = llm.create_chat_completion(