| """ |
| 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 |
|
|
| |
| try: |
| import ollama as _ollama |
| OLLAMA_AVAILABLE = True |
| except ImportError: |
| _ollama = None |
| OLLAMA_AVAILABLE = False |
|
|
| OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'llama3') |
|
|
| |
| 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: |
| return json.loads(text) |
| except Exception: |
| pass |
| |
| clean = re.sub(r'```(?:json)?', '', text).strip().rstrip('`').strip() |
| try: |
| return json.loads(clean) |
| except Exception: |
| pass |
| |
| 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. |
| """ |
| |
| text = _call_ollama(prompt) |
| if text: |
| return text |
| |
| if api_keys and api_keys.get('groq'): |
| text = _call_groq(prompt, api_keys['groq']) |
| if text: |
| return text |
| |
| if api_keys and api_keys.get('openai'): |
| text = _call_openai(prompt, api_keys['openai']) |
| if text: |
| return text |
| return None |
|
|
|
|
| |
| _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] |
| 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 |
|
|
|
|
| |
| _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 |
|
|
|
|
| |
| _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 |
|
|
|
|
| |
| 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." |
|
|