Keyword inspector card added.Cleaned up Search term code
Browse files- app.py +27 -1
- app/ads1/keyword_inspector.py +93 -0
- app/ads1/search_term_optimizer.py +0 -14
app.py
CHANGED
|
@@ -12,6 +12,7 @@ print("IMPORT 3 OK", flush=True)
|
|
| 12 |
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 |
|
| 16 |
# ==================================================
|
| 17 |
# ROMER / ADVISOR DASHBOARD THEME
|
|
@@ -978,6 +979,22 @@ def run_search_term_optimizer_card(state):
|
|
| 978 |
except Exception as e:
|
| 979 |
return f"Search term optimization failed: {e}"
|
| 980 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 981 |
|
| 982 |
# ==================================================
|
| 983 |
# UI
|
|
@@ -1021,7 +1038,10 @@ with gr.Blocks(fill_height=True, fill_width=True, css=CSS) as demo:
|
|
| 1021 |
elem_classes=["ai-button-card"],
|
| 1022 |
)
|
| 1023 |
gr.HTML(ai_card("Budget Optimizer", "Where to adjust spend?"))
|
| 1024 |
-
|
|
|
|
|
|
|
|
|
|
| 1025 |
|
| 1026 |
with gr.Row(elem_classes=["ai-row"]):
|
| 1027 |
search_term_card = gr.Button(
|
|
@@ -1057,6 +1077,12 @@ with gr.Blocks(fill_height=True, fill_width=True, css=CSS) as demo:
|
|
| 1057 |
outputs=ads_output,
|
| 1058 |
)
|
| 1059 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1060 |
demo.load(
|
| 1061 |
fn=initial_data_load,
|
| 1062 |
outputs=[full_state, campaign_picker, hero_html, kpi_html],
|
|
|
|
| 12 |
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
|
|
|
|
| 979 |
except Exception as e:
|
| 980 |
return f"Search term optimization failed: {e}"
|
| 981 |
|
| 982 |
+
@spaces.GPU(duration=120)
|
| 983 |
+
def run_keyword_inspector_card(state):
|
| 984 |
+
try:
|
| 985 |
+
if not state:
|
| 986 |
+
return "Select a campaign first."
|
| 987 |
+
|
| 988 |
+
dfs = state.get("full_dfs")
|
| 989 |
+
campaign_name = state.get("campaign_name")
|
| 990 |
+
|
| 991 |
+
if dfs is None or campaign_name is None:
|
| 992 |
+
return "Campaign state is not properly initialized."
|
| 993 |
+
|
| 994 |
+
return run_keyword_inspector(dfs, campaign_name=campaign_name)
|
| 995 |
+
|
| 996 |
+
except Exception as e:
|
| 997 |
+
return f"Search term optimization failed: {e}"
|
| 998 |
|
| 999 |
# ==================================================
|
| 1000 |
# UI
|
|
|
|
| 1038 |
elem_classes=["ai-button-card"],
|
| 1039 |
)
|
| 1040 |
gr.HTML(ai_card("Budget Optimizer", "Where to adjust spend?"))
|
| 1041 |
+
keyword_inspector_card = gr.Button(
|
| 1042 |
+
value="Keyword Inspector\n <hr> Winning versus wasting keywords..",
|
| 1043 |
+
elem_classes=["ai-button-card"],
|
| 1044 |
+
)
|
| 1045 |
|
| 1046 |
with gr.Row(elem_classes=["ai-row"]):
|
| 1047 |
search_term_card = gr.Button(
|
|
|
|
| 1077 |
outputs=ads_output,
|
| 1078 |
)
|
| 1079 |
|
| 1080 |
+
keyword_inspector_card.click(
|
| 1081 |
+
fn=run_keyword_inspector_card,
|
| 1082 |
+
inputs=campaign_state,
|
| 1083 |
+
outputs=ads_output,
|
| 1084 |
+
)
|
| 1085 |
+
|
| 1086 |
demo.load(
|
| 1087 |
fn=initial_data_load,
|
| 1088 |
outputs=[full_state, campaign_picker, hero_html, kpi_html],
|
app/ads1/keyword_inspector.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import pandas as pd
|
| 3 |
+
|
| 4 |
+
from app.recs.generate import generate_explanation, is_bad_llm_output
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
# -------------------------
|
| 8 |
+
# Feature engineering only
|
| 9 |
+
# -------------------------
|
| 10 |
+
def build_keyword_features(df: pd.DataFrame) -> pd.DataFrame:
|
| 11 |
+
df = df.copy()
|
| 12 |
+
|
| 13 |
+
df["cost"] = df["cost"].fillna(0)
|
| 14 |
+
df["clicks"] = df["clicks"].fillna(0)
|
| 15 |
+
df["impressions"] = df["impressions"].fillna(0)
|
| 16 |
+
df["conversions"] = df.get("conversions", 0).fillna(0)
|
| 17 |
+
|
| 18 |
+
df["ctr"] = (df["clicks"] / df["impressions"].replace(0, 1)) * 100
|
| 19 |
+
df["cpa"] = df["cost"] / df["conversions"].replace(0, 1)
|
| 20 |
+
|
| 21 |
+
return df
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# -------------------------
|
| 25 |
+
# Prompt (simplified + stronger reasoning)
|
| 26 |
+
# -------------------------
|
| 27 |
+
def build_keyword_prompt(context: dict) -> str:
|
| 28 |
+
payload = json.dumps(context, indent=2, default=str)
|
| 29 |
+
|
| 30 |
+
return f"""
|
| 31 |
+
You are an expert Google Ads performance strategist for a preschool business.
|
| 32 |
+
|
| 33 |
+
Campaign: {context.get("campaign_name", "ALL CAMPAIGNS")}
|
| 34 |
+
|
| 35 |
+
Your job:
|
| 36 |
+
Analyze keyword performance and identify:
|
| 37 |
+
- Winning keywords (high intent + conversions)
|
| 38 |
+
- Wasted spend keywords (cost but no conversions)
|
| 39 |
+
- Keywords to scale
|
| 40 |
+
- Keywords to pause or reduce bids
|
| 41 |
+
- Any CTR / conversion anomalies
|
| 42 |
+
|
| 43 |
+
IMPORTANT:
|
| 44 |
+
- Do NOT assume CRM / SaaS context
|
| 45 |
+
- Assume all data relates to preschool admissions, daycare, or childcare services
|
| 46 |
+
- Think like a preschool marketing expert
|
| 47 |
+
|
| 48 |
+
DATA:
|
| 49 |
+
{payload}
|
| 50 |
+
|
| 51 |
+
Return 5 bullet points.
|
| 52 |
+
Each bullet must start with "- ".
|
| 53 |
+
Be direct, business-focused, no intro text.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# -------------------------
|
| 58 |
+
# Main runner
|
| 59 |
+
# -------------------------
|
| 60 |
+
def run_keyword_inspector(dfs: dict, campaign_name: str | None = None) -> str:
|
| 61 |
+
print("\n🚀 [keyword_inspector] STARTED", flush=True)
|
| 62 |
+
|
| 63 |
+
if not dfs or "keywords" not in dfs:
|
| 64 |
+
return "⚠️ No keyword data available."
|
| 65 |
+
|
| 66 |
+
df = dfs["keywords"].copy()
|
| 67 |
+
df = build_keyword_features(df)
|
| 68 |
+
|
| 69 |
+
# optional campaign filter (safe, not destructive)
|
| 70 |
+
if campaign_name and "campaign_name" in df.columns:
|
| 71 |
+
df = df[df["campaign_name"] == campaign_name]
|
| 72 |
+
|
| 73 |
+
context = {
|
| 74 |
+
"campaign_name": campaign_name,
|
| 75 |
+
"keywords": df.to_dict("records") # FULL DATA given to LLM
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
print("🧠 [keyword_inspector] context built", flush=True)
|
| 79 |
+
|
| 80 |
+
prompt = build_keyword_prompt(context)
|
| 81 |
+
print("✍️ [keyword_inspector] prompt built", flush=True)
|
| 82 |
+
|
| 83 |
+
result = generate_explanation(prompt)
|
| 84 |
+
|
| 85 |
+
if is_bad_llm_output(result):
|
| 86 |
+
print("⚠️ [keyword_inspector] LLM fallback triggered", flush=True)
|
| 87 |
+
return (
|
| 88 |
+
"- Unable to generate LLM insights right now.\n"
|
| 89 |
+
"- Check keyword data quality or retry."
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
print("📤 [keyword_inspector] result received", flush=True)
|
| 93 |
+
return result
|
app/ads1/search_term_optimizer.py
CHANGED
|
@@ -43,20 +43,6 @@ def detect_review_terms(df):
|
|
| 43 |
def detect_scaling_terms(df):
|
| 44 |
return df[(df["conversions"] > 0)].sort_values("cpa", ascending=True)
|
| 45 |
|
| 46 |
-
# def classify_search_term(dfs: dict) -> dict:
|
| 47 |
-
# df = dfs["search_terms"].copy()
|
| 48 |
-
# df = build_search_term_features(df)
|
| 49 |
-
|
| 50 |
-
# negatives = detect_negative_terms(df)
|
| 51 |
-
# review = detect_review_terms(df)
|
| 52 |
-
# winners = detect_scaling_terms(df)
|
| 53 |
-
|
| 54 |
-
# return {
|
| 55 |
-
# "negative_keywords": negatives.to_dict("records"),
|
| 56 |
-
# "review_terms": review.to_dict("records"),
|
| 57 |
-
# "winning_terms": winners.head(10).to_dict("records"),
|
| 58 |
-
# }
|
| 59 |
-
|
| 60 |
def build_search_optimizer_prompt(context: dict) -> str:
|
| 61 |
# Convert data to clean JSON string strings for better LLM readability
|
| 62 |
neg_json = json.dumps(context['negative_keywords'], indent=2)
|
|
|
|
| 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)
|