last_edit / server /ads_ai.py
Moharek
Deploy Moharek GEO Platform
a74b879
"""
Ads AI Layer โ€” AI-powered bidding suggestions, ad copy generation,
and negative keyword detection.
Backends (in priority order): Ollama (free/local) โ†’ Groq โ†’ OpenAI
"""
import json
import os
from typing import List, Dict, Optional
# โ”€โ”€ Ollama (free, local) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
try:
import ollama as _ollama
OLLAMA_AVAILABLE = True
except ImportError:
_ollama = None
OLLAMA_AVAILABLE = False
OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'llama3')
# โ”€โ”€ Groq / OpenAI (cloud) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
try:
from server import ai_analysis
except ImportError:
ai_analysis = None
def _parse_json(text: str):
"""Extract first JSON object or array from a text response."""
import re
# try direct parse first
try:
return json.loads(text)
except Exception:
pass
# strip markdown fences
clean = re.sub(r'```(?:json)?', '', text).strip().rstrip('`').strip()
try:
return json.loads(clean)
except Exception:
pass
# find first {...} or [...]
for pattern in (r'(\[.*\])', r'(\{.*\})'):
m = re.search(pattern, clean, re.DOTALL)
if m:
try:
return json.loads(m.group(1))
except Exception:
pass
return None
def _call_ollama(prompt: str) -> Optional[str]:
"""Call local Ollama. Returns raw text or None."""
if not OLLAMA_AVAILABLE:
return None
try:
resp = _ollama.chat(
model=OLLAMA_MODEL,
messages=[{'role': 'user', 'content': prompt}]
)
return resp['message']['content']
except Exception as e:
print(f'[AdsAI] Ollama error: {e}')
return None
def _call_groq(prompt: str, api_key: str) -> Optional[str]:
"""Call Groq API. Returns raw text or None."""
try:
from groq import Groq
client = Groq(api_key=api_key)
resp = client.chat.completions.create(
model=os.getenv('GROQ_MODEL', 'llama-3.1-8b-instant'),
messages=[{'role': 'user', 'content': prompt}],
temperature=0.2,
max_completion_tokens=2048,
stream=False
)
return resp.choices[0].message.content
except Exception as e:
print(f'[AdsAI] Groq error: {e}')
return None
def _call_openai(prompt: str, api_key: str) -> Optional[str]:
"""Call OpenAI API. Returns raw text or None."""
try:
import openai
client = openai.OpenAI(api_key=api_key)
resp = client.chat.completions.create(
model=os.getenv('OPENAI_MODEL', 'gpt-4o-mini'),
messages=[{'role': 'user', 'content': prompt}],
temperature=0.2,
max_tokens=2048
)
return resp.choices[0].message.content
except Exception as e:
print(f'[AdsAI] OpenAI error: {e}')
return None
def _call_ai(prompt: str, api_keys: dict) -> Optional[str]:
"""
Try backends in order: Ollama (free) โ†’ Groq โ†’ OpenAI.
Returns raw text string or None.
"""
# 1. Ollama โ€” always try first (free, no key needed)
text = _call_ollama(prompt)
if text:
return text
# 2. Groq
if api_keys and api_keys.get('groq'):
text = _call_groq(prompt, api_keys['groq'])
if text:
return text
# 3. OpenAI
if api_keys and api_keys.get('openai'):
text = _call_openai(prompt, api_keys['openai'])
if text:
return text
return None
# โ”€โ”€ 1. AI Bid Suggestions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
_DEMO_BID_SUGGESTIONS = [
{"keyword": "ุชุญุณูŠู† ู…ุญุฑูƒุงุช ุงู„ุจุญุซ", "current_cpc": 2.10, "suggested_cpc": 2.50,
"action": "increase", "reason": "Quality Score 8 + 12 conversions โ€” strong performer", "expected_impact": "+18% conversions"},
{"keyword": "ุดุฑูƒุฉ ุณูŠูˆ", "current_cpc": 1.60, "suggested_cpc": 1.80,
"action": "increase", "reason": "Good QS 7 with steady conversions", "expected_impact": "+10% impression share"},
{"keyword": "ุณูŠูˆ ุนุฑุจูŠ", "current_cpc": 0.95, "suggested_cpc": 0.65,
"action": "decrease", "reason": "Low QS (6) and only 2 conversions โ€” overbidding", "expected_impact": "-30% wasted spend"},
{"keyword": "keyword ranking tool", "current_cpc": 1.20, "suggested_cpc": 0.0,
"action": "pause", "reason": "QS 5 and zero conversions with $104 spend โ€” bleeding budget", "expected_impact": "Save $104/month"},
{"keyword": "SEO services Saudi Arabia", "current_cpc": 1.90, "suggested_cpc": 2.30,
"action": "increase", "reason": "Highest QS (9), best converter โ€” maximize exposure", "expected_impact": "+25% conversion volume"},
]
def ai_bid_suggestion(keyword_data: List[Dict], api_keys: dict = None) -> List[Dict]:
"""
Analyze keyword performance and suggest bid adjustments.
Returns list of action items: increase / decrease / pause / keep.
"""
if not api_keys:
return _DEMO_BID_SUGGESTIONS
sample = keyword_data[:20] # Limit for prompt size
prompt = f"""
You are a Google Ads bidding expert. Analyze these keywords and suggest optimal CPC bid adjustments.
Keywords data:
{json.dumps(sample, ensure_ascii=False, indent=2)}
For EACH keyword return an object with:
- keyword: string
- current_cpc: number
- suggested_cpc: number (0 if pausing)
- action: "increase" | "decrease" | "pause" | "keep"
- reason: one sentence explaining why (in English or Arabic)
- expected_impact: brief expected outcome
Rules:
- QS < 4 โ†’ suggest pause
- 0 conversions + cost > $50 โ†’ pause
- CTR < 1.5% and 0 conversions โ†’ decrease 25%
- Conversions > 0 and QS >= 7 โ†’ increase 15-25%
Return ONLY a valid JSON array. No extra text.
"""
res = _call_ai(prompt, api_keys)
if res and res.get('result') and isinstance(res['result'], list):
return res['result']
return _DEMO_BID_SUGGESTIONS
# โ”€โ”€ 2. Ad Copy Generator โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
_DEMO_AD_COPY = {
"headlines": [
"ุฎุฏู…ุงุช ุงู„ุณูŠูˆ ุงู„ุงุญุชุฑุงููŠุฉ",
"ุชุตุฏุฑ ู†ุชุงุฆุฌ ุฌูˆุฌู„ ู…ุน ู…ุญุฑูƒ",
"ู†ุชุงุฆุฌ ู…ุถู…ูˆู†ุฉ ููŠ 90 ูŠูˆู…",
"ุณูŠูˆ ุจุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ",
"ุฒุฏ ุฒูˆุงุฑ ู…ูˆู‚ุนูƒ 3 ุฃุถุนุงู",
"ุฎุจุฑุฉ 10 ุณู†ูˆุงุช ููŠ ุงู„ุณูŠูˆ ุงู„ุนุฑุจูŠ",
"ุงุญุตู„ ุนู„ู‰ ุนู…ู„ุงุก ุฃูƒุซุฑ ุงู„ูŠูˆู…",
"SEO ู„ู„ุดุฑูƒุงุช ุงู„ุณุนูˆุฏูŠุฉ",
"ุงุณุชุฑุงุชูŠุฌูŠุฉ ุณูŠูˆ ู…ุชูƒุงู…ู„ุฉ",
"ุธู‡ูˆุฑ ููŠ ChatGPT ูˆุฌูˆุฌู„",
],
"descriptions": [
"ู†ุญุณู† ุธู‡ูˆุฑ ู…ูˆู‚ุนูƒ ููŠ ุฌูˆุฌู„ ูˆChatGPT. ุงุญุตู„ ุนู„ู‰ ุนู…ู„ุงุก ุฃูƒุซุฑ ุจุชูƒู„ูุฉ ุฃู‚ู„.",
"ุฎุจุฑุฉ 10 ุณู†ูˆุงุช ููŠ ุงู„ุณูŠูˆ ุงู„ุนุฑุจูŠ. ุงุณุชุฑุงุชูŠุฌูŠุงุช ู…ุซุจุชุฉ ู„ู„ุดุฑูƒุงุช ุงู„ุณุนูˆุฏูŠุฉ.",
"ุชุญุณูŠู† ุดุงู…ู„ โ€” ุชู‚ู†ูŠุŒ ู…ุญุชูˆู‰ุŒ ุฑูˆุงุจุท. ู†ุชุงุฆุฌ ู‚ุงุจู„ุฉ ู„ู„ู‚ูŠุงุณ.",
"ุชูˆุงุตู„ ู…ุนู†ุง ุงู„ุขู† ูˆุงุญุตู„ ุนู„ู‰ ุชุญู„ูŠู„ ู…ุฌุงู†ูŠ ู„ู…ูˆู‚ุนูƒ.",
],
"display_path": ["ุณูŠูˆ", "ุฎุฏู…ุงุช-ุงุญุชุฑุงููŠุฉ"]
}
def generate_ad_copy(service_name: str, usp: str, target_audience: str,
lang: str = "ar", api_keys: dict = None) -> dict:
"""
Generate Responsive Search Ad (RSA) copy.
Returns headlines, descriptions, and display paths.
"""
if not api_keys:
return _DEMO_AD_COPY
lang_label = "Arabic" if lang == "ar" else "English"
prompt = f"""
Generate Google Ads Responsive Search Ad (RSA) copy.
Service: {service_name}
USP: {usp}
Target audience: {target_audience}
Language: {lang_label}
Rules:
- Headlines: max 30 characters EACH (this is a hard limit)
- Descriptions: max 90 characters EACH (hard limit)
- Include CTA in at least 2 headlines
- Include the main keyword/service naturally
- Be compelling and specific โ€” avoid generic phrases
Return ONLY valid JSON in this exact format:
{{
"headlines": ["h1", "h2", ... up to 15],
"descriptions": ["d1", "d2", "d3", "d4"],
"display_path": ["path1", "path2"]
}}
"""
res = _call_ai(prompt, api_keys)
if res and res.get('result') and isinstance(res['result'], dict):
return res['result']
return _DEMO_AD_COPY
# โ”€โ”€ 3. Negative Keyword Detector โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
_DEMO_NEGATIVES = {
"negatives_exact": ["SEO salary", "SEO jobs", "learn SEO free", "SEO course"],
"negatives_phrase": ["how to SEO", "SEO tutorial", "SEO book"],
"keep_monitoring": ["SEO agency review", "SEO comparison"],
"estimated_savings": "$180/month"
}
def detect_negative_keywords(search_terms_data: dict, api_keys: dict = None,
business_context: str = "B2B SEO/digital marketing in Saudi Arabia") -> dict:
"""
Analyze search terms report and identify negatives to add.
Returns exact negatives, phrase negatives, and estimated savings.
"""
if not api_keys:
return _DEMO_NEGATIVES
wasted = search_terms_data.get('wasted_spend', [])
if not wasted:
return {"negatives_exact": [], "negatives_phrase": [], "keep_monitoring": [], "estimated_savings": "$0"}
wasted_terms = [t.get("term", "") for t in wasted]
wasted_spend = sum(t.get("clicks", 0) * t.get("avg_cpc", 0) for t in wasted)
prompt = f"""
Analyze these search terms that triggered ads but got ZERO conversions.
Identify which ones are definitely irrelevant and should be added as negative keywords.
Business context: {business_context}
Wasted search terms:
{json.dumps(wasted_terms, ensure_ascii=False)}
Return ONLY valid JSON:
{{
"negatives_exact": ["term1", ...],
"negatives_phrase": ["phrase1", ...],
"keep_monitoring": ["term_to_watch", ...],
"estimated_savings": "$X/month"
}}
"""
res = _call_ai(prompt, api_keys)
if res and res.get('result') and isinstance(res['result'], dict):
result = res['result']
result['estimated_savings'] = f"${round(wasted_spend, 2)}/period"
return result
return _DEMO_NEGATIVES
# โ”€โ”€ 4. Weekly AI Performance Report โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def generate_weekly_report(campaigns: list, keywords: list, search_terms: dict,
api_keys: dict = None, lang: str = "ar") -> str:
"""
Generate a comprehensive AI weekly performance report.
Returns markdown-formatted analysis text.
"""
if not api_keys:
return """## ุชู‚ุฑูŠุฑ ุงู„ุฃุฏุงุก ุงู„ุฃุณุจูˆุนูŠ โ€” ู†ู…ูˆุฐุฌ ุชูˆุถูŠุญูŠ
### ู…ู„ุฎุต ุงู„ุฃุฏุงุก
- ุฅุฌู…ุงู„ูŠ ุงู„ู†ู‚ุฑุงุช: **1,555 ู†ู‚ุฑุฉ** (+12% ุนู† ุงู„ุฃุณุจูˆุน ุงู„ู…ุงุถูŠ)
- ุฅุฌู…ุงู„ูŠ ุงู„ุชุญูˆูŠู„ุงุช: **46 ุชุญูˆูŠู„** ุจู…ุชูˆุณุท ุชูƒู„ูุฉ **$60.5/ุชุญูˆูŠู„**
- ุฅุฌู…ุงู„ูŠ ุงู„ุฅู†ูุงู‚: **$2,799** ุฎู„ุงู„ ุขุฎุฑ 30 ูŠูˆู…
### ุฃุจุฑุฒ ุงู„ุฅู†ุฌุงุฒุงุช
โœ… ุญู…ู„ุฉ "SEO Services SA" ุญู‚ู‚ุช ุฃุนู„ู‰ ู†ุณุจุฉ ุชุญูˆูŠู„ (2.73%)
โœ… ูƒู„ู…ุฉ "SEO services Saudi Arabia" ุจุฌูˆุฏุฉ ุฅุนู„ุงู† 9 โ€” ุงู„ุฃูุถู„ ุฃุฏุงุกู‹
โœ… ุงู†ุฎูุงุถ ุชูƒู„ูุฉ ุงู„ุชุญูˆูŠู„ ุจู†ุณุจุฉ 8% ู…ู‚ุงุฑู†ุฉ ุจุงู„ุดู‡ุฑ ุงู„ู…ุงุถูŠ
### ุงู„ู…ุดุงูƒู„ ูˆุงู„ูุฑุต
โš ๏ธ ูƒู„ู…ุฉ "keyword ranking tool" ุชุณุชู‡ู„ูƒ $104 ุจุฏูˆู† ุฃูŠ ุชุญูˆูŠู„ โ†’ ูŠููˆุตู‰ ุจุงู„ุฅูŠู‚ุงู
โš ๏ธ ู†ุณุจุฉ ุธู‡ูˆุฑ ุญู…ู„ุฉ "Brand Keywords" ู…ู†ุฎูุถุฉ (41%) โ†’ ุฒูŠุงุฏุฉ ุงู„ู…ูŠุฒุงู†ูŠุฉ
๐Ÿ”ฅ ูุฑุตุฉ: ูƒู„ู…ุงุช ุงู„ุณูŠูˆ ุงู„ุนุฑุจูŠุฉ ุชูุญู‚ู‚ CPA ุฃู‚ู„ 30% ู…ู† ุงู„ุฅู†ุฌู„ูŠุฒูŠุฉ
### ุงู„ุชูˆุตูŠุงุช ู„ู„ุฃุณุจูˆุน ุงู„ู‚ุงุฏู…
1. ุฑูุน ุนุฑุถ "ุชุญุณูŠู† ู…ุญุฑูƒุงุช ุงู„ุจุญุซ" ู…ู† $2.10 ุฅู„ู‰ $2.50
2. ุฅูŠู‚ุงู ูƒู„ู…ุฉ "keyword ranking tool" ููˆุฑุงู‹ (ุชูˆููŠุฑ $104)
3. ุฅุถุงูุฉ ูƒู„ู…ุงุช ุณู„ุจูŠุฉ: "ูˆุธุงุฆู ุณูŠูˆ"ุŒ "ุชุนู„ู… ุณูŠูˆ"ุŒ "ูƒูˆุฑุณ ุณูŠูˆ"
4. ุชูุนูŠู„ ุญู…ู„ุฉ Brand Keywords ุจู…ูŠุฒุงู†ูŠุฉ $3/ูŠูˆู…
### ุงู„ูƒู„ู…ุงุช ุงู„ุณู„ุจูŠุฉ ุงู„ู…ู‚ุชุฑุญุฉ
`SEO jobs` ยท `SEO salary` ยท `learn SEO free` ยท `ุณูŠูˆ ู…ุฌุงู†ูŠ` ยท `ูƒูˆุฑุณ ุณูŠูˆ`"""
top_camps = campaigns[:3]
top_kws = keywords[:10]
wasted = search_terms.get('wasted_spend', [])[:5]
lang_label = "Arabic" if lang == "ar" else "English"
prompt = f"""
You are a Google Ads expert. Write a clear, actionable weekly performance report.
Language: {lang_label}
Campaign Performance:
{json.dumps(top_camps, ensure_ascii=False, indent=2)}
Top Keywords:
{json.dumps(top_kws, ensure_ascii=False, indent=2)}
Wasted Spend Terms (no conversions):
{json.dumps(wasted, ensure_ascii=False, indent=2)}
Write a professional report with these sections (use markdown, be specific with numbers):
1. ู…ู„ุฎุต ุงู„ุฃุฏุงุก / Performance Summary
2. ุฃุจุฑุฒ ุงู„ุฅู†ุฌุงุฒุงุช / Key Wins
3. ุงู„ู…ุดุงูƒู„ ูˆุงู„ูุฑุต / Issues & Opportunities
4. ุงู„ุชูˆุตูŠุงุช / Recommendations
5. ุงู„ูƒู„ู…ุงุช ุงู„ุณู„ุจูŠุฉ ุงู„ู…ู‚ุชุฑุญุฉ / Suggested Negatives
Be specific. Use actual numbers from the data above.
"""
res = _call_ai(prompt, api_keys)
if res and isinstance(res.get('result'), str):
return res['result']
elif res and res.get('raw'):
return res['raw']
return "Report generation failed โ€” check your API key."