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

Revert "Prompts updated"

Browse files

This reverts commit 41cae8c65962b4eaa5789a1ec0452c6e335500e7.

app/ads1/ads_analyst.py CHANGED
@@ -134,14 +134,9 @@ 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 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
 
 
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
 
app/ads1/budget_optimizer.py CHANGED
@@ -28,16 +28,12 @@ def build_budget_optimizer_context(dfs: dict, campaign_name: str | None = None):
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,43 +64,6 @@ 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,7 +82,10 @@ def run_budget_optimizer(dfs: dict, campaign_name: str | None = None) -> str:
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
 
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
  )
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
 
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
app/ads1/growth_finder.py CHANGED
@@ -28,19 +28,12 @@ def build_growth_finder_context(dfs: dict, campaign_name: str | None = None):
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,33 +41,28 @@ 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,7 +81,10 @@ def run_growth_finder(dfs: dict, campaign_name: str | None = None) -> str:
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
 
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
  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
 
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
app/ads1/keyword_inspector.py CHANGED
@@ -47,43 +47,6 @@ def build_keyword_prompt(context: dict) -> str:
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,21 +63,9 @@ def run_keyword_inspector(dfs: dict, campaign_name: str | None = None) -> str:
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,7 +77,10 @@ def run_keyword_inspector(dfs: dict, campaign_name: str | None = None) -> str:
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
 
47
  DATA:
48
  {payload}
49
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  # -------------------------
51
  # Main runner
52
  # -------------------------
 
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
 
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
app/ads1/search_term_optimizer.py CHANGED
@@ -34,13 +34,7 @@ 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
- 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,22 +66,6 @@ def build_search_optimizer_prompt(context: dict) -> str:
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,39 +77,13 @@ def build_search_optimizer_context(dfs: dict, campaign_name: str | None = None):
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,7 +105,10 @@ def run_search_term_optimizer(dfs: dict, campaign_name: str | None = None) -> st
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
 
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
  """
67
  )
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  # -------------------------
70
  # Context builder (FULL DATA approach)
71
  # -------------------------
 
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
 
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
app/recs/generate.py CHANGED
@@ -18,10 +18,9 @@ _infer_lock = threading.Lock()
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,20 +45,6 @@ def _looks_like_garbage(text: str) -> bool:
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,7 +107,7 @@ def _message_text(message: dict) -> str:
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(
 
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
  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
 
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(