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

Search term optimizer logic simplified for direct llm analysis

Browse files
Files changed (1) hide show
  1. app/ads1/search_term_optimizer.py +77 -66
app/ads1/search_term_optimizer.py CHANGED
@@ -1,99 +1,110 @@
1
- import pandas as pd
2
- from app.recs.generate import TARGET_CPL, generate_explanation, is_bad_llm_output
3
  import json
 
 
4
 
 
 
 
5
  def build_search_term_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
  if "conversions" not in df.columns:
12
  df["conversions"] = 0
13
 
14
  df["conversions"] = df["conversions"].fillna(0)
15
- df["ctr"] = ((df["clicks"] / df["impressions"])*100).replace(0, 1)
16
- df["cvr"] = ((df["conversions"] / df["clicks"])*100).replace(0, 1)
 
 
17
  df["cpc"] = df["cost"] / df["clicks"].replace(0, 1)
18
  df["cpa"] = df["cost"] / df["conversions"].replace(0, 1)
19
 
20
  return df
21
 
22
- def _search_terms_for_campaign(dfs: dict, campaign_name: str | None) -> pd.DataFrame:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  df = dfs["search_terms"].copy()
24
- if (campaign_name
25
- and "campaign_name" in df.columns):
26
- df = df[df["campaign_name"] == campaign_name]
27
- return df
28
 
29
- def detect_negative_terms(df):
30
- return df[
31
- (df["clicks"] >= 20) &
32
- (df["cost"] >= 5) &
33
- (df["conversions"] == 0)
34
- ]
35
 
36
- def detect_review_terms(df):
37
- return df[
38
- (df["clicks"] >= 10) &
39
- (df["ctr"] < 2) &
40
- (df["conversions"] == 0)
41
- ]
42
 
43
- def detect_scaling_terms(df):
44
- return df[(df["conversions"] > 0)].sort_values("cpa", ascending=True)
 
 
45
 
46
- def build_search_optimizer_prompt(context: dict) -> str:
47
- # Convert data to clean JSON string strings for better LLM readability
48
- neg_json = json.dumps(context['negative_keywords'], indent=2)
49
- rev_json = json.dumps(context['review_terms'], indent=2)
50
- win_json = json.dumps(context['winning_terms'], indent=2)
51
 
52
- return f"""You are an expert Google Ads optimization engine.
53
- Analyze the following three categories of search terms for the campaign "{context['campaign_name'] or 'All Campaigns'}":
 
 
 
54
 
55
- ### HIGH WASTE TERMS (Add as negatives)
56
- {neg_json}
57
 
58
- ### LOW CTR REVIEW TERMS (Needs attention)
59
- {rev_json}
60
 
61
- ### WINNING SCALING TERMS (Profitable)
62
- {win_json}
63
 
64
- Provide your analysis in a strict JSON format with this exact schema:
65
- {{
66
- "waste_reduction_actions": ["action 1", "action 2"],
67
- "scaling_opportunities": ["opportunity 1", "opportunity 2"],
68
- "immediate_negative_keywords": ["keyword 1", "keyword 2"]
69
- }}
70
- Return ONLY valid JSON. Do not include markdown code blocks, explanations, or extra text."""
71
 
72
- def build_search_optimizer_context(dfs: dict, campaign_name: str | None = None):
73
- df = _search_terms_for_campaign(dfs, campaign_name)
74
- df = build_search_term_features(df)
75
- negatives = detect_negative_terms(df)
76
- review = detect_review_terms(df)
77
- winners = detect_scaling_terms(df)
78
 
79
- return {
80
- "campaign_name": campaign_name,
81
- "negative_keywords": negatives.head(10).to_dict("records"),
82
- "review_terms": review.head(10).to_dict("records"),
83
- "winning_terms": winners.head(10).to_dict("records"),
84
- }
85
 
86
- def run_search_term_optimizer(dfs: dict,campaign_name: str | None = None) -> str:
87
- print("\n🚀 [search_term_card] STARTED", flush=True)
88
- if not dfs:
89
  return (
90
- "⚠️ No campaign data "
91
- "select a campaign first."
92
  )
93
- context = build_search_optimizer_context(dfs,campaign_name)
94
- print("🧠 [search_term_card] context built",flush=True)
95
- prompt = build_search_optimizer_prompt(context)
96
- print("✍️ [search_term_card] prompt built",flush=True)
97
- result = generate_explanation(prompt)
98
- print("📤 [search_term_card] result received", flush=True)
99
  return result
 
 
 
1
  import json
2
+ import pandas as pd
3
+ from app.recs.generate import generate_explanation, is_bad_llm_output
4
 
5
+ # -------------------------
6
+ # Feature engineering only
7
+ # -------------------------
8
  def build_search_term_features(df: pd.DataFrame) -> pd.DataFrame:
9
  df = df.copy()
10
 
11
  df["cost"] = df["cost"].fillna(0)
12
  df["clicks"] = df["clicks"].fillna(0)
13
  df["impressions"] = df["impressions"].fillna(0)
14
+
15
  if "conversions" not in df.columns:
16
  df["conversions"] = 0
17
 
18
  df["conversions"] = df["conversions"].fillna(0)
19
+
20
+ # Core metrics
21
+ df["ctr"] = (df["clicks"] / df["impressions"].replace(0, 1)) * 100
22
+ df["cvr"] = (df["conversions"] / df["clicks"].replace(0, 1)) * 100
23
  df["cpc"] = df["cost"] / df["clicks"].replace(0, 1)
24
  df["cpa"] = df["cost"] / df["conversions"].replace(0, 1)
25
 
26
  return df
27
 
28
+
29
+ # -------------------------
30
+ # Optional: lightweight pruning (NOT rule-based logic)
31
+ # -------------------------
32
+ def prepare_context_df(df: pd.DataFrame, max_rows: int = 200) -> pd.DataFrame:
33
+ """
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)
41
+ # -------------------------
42
+ def build_search_optimizer_prompt(context: dict) -> str:
43
+ payload = json.dumps(context, indent=2, default=str)
44
+ name = context.get("campaign_name", "this campaign")
45
+
46
+ return (
47
+ f"You are an expert Google Ads search term optimization strategist.\n\n"
48
+ f"Analyze search term performance for {name}.\n\n"
49
+ "Your job is to provide 3 to 5 actionable insights.\n"
50
+ "Focus on:\n"
51
+ "- wasted spend search terms\n"
52
+ "- high intent converting terms\n"
53
+ "- scaling opportunities\n"
54
+ "- terms to pause or reduce bids\n"
55
+ "- unusual CTR / CPA / conversion patterns\n\n"
56
+ "Rules:\n"
57
+ "- Be specific and actionable\n"
58
+ "- Use simple language\n"
59
+ "- One insight per bullet point\n"
60
+ "- Start each line with '- '\n"
61
+ "- No intro sentence, no summary, no JSON\n\n"
62
+ f"Data:\n{payload}"
63
+ )
64
+
65
+ # -------------------------
66
+ # Context builder (FULL DATA approach)
67
+ # -------------------------
68
+ def build_search_optimizer_context(dfs: dict, campaign_name: str | None = None):
69
  df = dfs["search_terms"].copy()
 
 
 
 
70
 
71
+ if campaign_name and "campaign_name" in df.columns:
72
+ df = df[df["campaign_name"] == campaign_name]
 
 
 
 
73
 
74
+ df = build_search_term_features(df)
75
+ df = prepare_context_df(df)
 
 
 
 
76
 
77
+ return {
78
+ "campaign_name": campaign_name,
79
+ "search_terms": df.to_dict("records") # FULL dataset context (bounded)
80
+ }
81
 
 
 
 
 
 
82
 
83
+ # -------------------------
84
+ # Runner
85
+ # -------------------------
86
+ def run_search_term_optimizer(dfs: dict, campaign_name: str | None = None) -> str:
87
+ print("\n🚀 [search_term_optimizer] STARTED", flush=True)
88
 
89
+ if not dfs or "search_terms" not in dfs:
90
+ return "⚠️ No search term data — select a campaign first."
91
 
92
+ context = build_search_optimizer_context(dfs, campaign_name)
 
93
 
94
+ print("🧠 [search_term_optimizer] context built", flush=True)
 
95
 
96
+ prompt = build_search_optimizer_prompt(context)
 
 
 
 
 
 
97
 
98
+ print("✍️ [search_term_optimizer] prompt built", flush=True)
 
 
 
 
 
99
 
100
+ result = generate_explanation(prompt)
 
 
 
 
 
101
 
102
+ if is_bad_llm_output(result):
103
+ print("⚠️ [search_term_optimizer] LLM fallback triggered", flush=True)
 
104
  return (
105
+ "- Unable to generate insights right now.\n"
106
+ "- Try again or check data quality."
107
  )
108
+
109
+ print("📤 [search_term_optimizer] result received", flush=True)
 
 
 
 
110
  return result