Revert "Prompts updated"
Browse filesThis reverts commit 41cae8c65962b4eaa5789a1ec0452c6e335500e7.
- app/ads1/ads_analyst.py +3 -8
- app/ads1/budget_optimizer.py +8 -46
- app/ads1/growth_finder.py +28 -37
- app/ads1/keyword_inspector.py +6 -52
- app/ads1/search_term_optimizer.py +7 -52
- app/recs/generate.py +4 -19
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
|
| 138 |
-
"
|
| 139 |
-
"
|
| 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 |
-
|
| 32 |
-
|
| 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
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 32 |
-
|
| 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
|
| 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"
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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
|
| 22 |
-
"Each bullet must be one short,
|
| 23 |
-
"
|
| 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.
|
| 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(
|