File size: 7,480 Bytes
a74b879
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
import os
import json
import requests
from typing import List, Dict, Any

try:
    from groq import Groq
except ImportError:
    Groq = None

try:
    import openai
except ImportError:
    openai = None

# Import the new smart action engine
try:
    from server.smart_action_engine import generate_smart_action_plan
except ImportError:
    generate_smart_action_plan = None

def _run_rule_engine(audit_data: dict) -> List[Dict[str, str]]:
    """
    Step 1: Simple Rule Engine - Arabic output (Legacy fallback)
    """
    actions = []
    pages = audit_data.get('pages', [])
    if not pages:
        return actions

    for page in pages:
        url = page.get('url', '')
        tags = [h.get('tag', '') for h in page.get('headings', [])]
        if 'h1' not in tags:
            actions.append({
                "type": "technical",
                "task": f"أضف وسم H1 إلى الصفحة: {url}",
                "priority": "high"
            })
        paras = page.get('paragraphs', [])
        if paras:
            avg_words = sum(len(str(p).split()) for p in paras) / len(paras)
            if avg_words < 20:
                actions.append({
                    "type": "content",
                    "task": f"توسيع المحتوى الضعيف في {url} (متوسط الكلمات: {int(avg_words)} كلمة/فقرة)",
                    "priority": "medium"
                })
        else:
            actions.append({
                "type": "content",
                "task": f"أضف فقرات نصية ومحتوى كافياً إلى الصفحة: {url}",
                "priority": "high"
            })

    ai_vis = audit_data.get('ai_visibility', {})
    if ai_vis and not ai_vis.get('results', []):
        actions.append({
            "type": "authority",
            "task": "لا يوجد ظهور في الذكاء الاصطناعي! أنشئ فقرة 'من نحن' قوية وانشر بيانات منظمة JSON-LD",
            "priority": "high"
        })

    return actions

def _call_ollama(prompt: str, model: str = "mistral") -> str:
    """Call local Ollama instance."""
    try:
        url = "http://localhost:11434/api/generate"
        payload = {
            "model": model,
            "prompt": prompt,
            "stream": False,
            "format": "json"
        }
        resp = requests.post(url, json=payload, timeout=30)
        resp.raise_for_status()
        return resp.json().get("response", "")
    except Exception as e:
        print(f"Ollama error: {e}")
        return ""

def _call_groq(prompt: str, api_key: str) -> str:
    if not Groq or not api_key:
        return ""
    try:
        client = Groq(api_key=api_key)
        completion = client.chat.completions.create(
            model=os.getenv('GROQ_MODEL', 'llama-3.3-70b-versatile'),
            messages=[
                {'role': 'system', 'content': 'You are an AI Growth Engine. Output valid JSON only.'},
                {'role': 'user', 'content': prompt}
            ],
            response_format={"type": "json_object"},
            temperature=0.2
        )
        return completion.choices[0].message.content
    except Exception as e:
        print(f"Groq logic error: {e}")
        return ""

def _call_openai(prompt: str, api_key: str) -> str:
    if not openai or not api_key:
        return ""
    try:
        client = openai.OpenAI(api_key=api_key)
        completion = client.chat.completions.create(
            model=os.getenv('OPENAI_MODEL', 'gpt-4o-mini'),
            messages=[
                {'role': 'system', 'content': 'You are an AI Growth Engine. Output valid JSON only.'},
                {'role': 'user', 'content': prompt}
            ],
            response_format={"type": "json_object"},
            temperature=0.2
        )
        return completion.choices[0].message.content
    except Exception as e:
        print(f"OpenAI logic error: {e}")
        return ""

def generate_action_plan(audit_data: dict, api_keys: dict = None) -> dict:
    """
    Main Action Engine logic - Now uses Smart Action Engine for enhanced recommendations
    """
    api_keys = api_keys or {}
    
    # Try to use the new smart action engine first
    if generate_smart_action_plan:
        try:
            smart_result = generate_smart_action_plan(audit_data, api_keys)
            if smart_result.get('ok'):
                print("✅ Using Smart Action Engine")
                return smart_result
        except Exception as e:
            print(f"⚠️ Smart Action Engine failed: {e}, falling back to legacy engine")
    
    # Fallback to legacy engine
    print("📋 Using Legacy Action Engine")
    
    # 1. Gather rules-based actions
    rule_actions = _run_rule_engine(audit_data)
    
    # 2. Build summary prompt for AI
    brand = audit_data.get('org_name', 'Unknown')
    pages = audit_data.get('pages', [])
    ai_vis = audit_data.get('ai_visibility', {})
    
    page_summary = f"Pages crawled: {len(pages)}. "
    if pages:
        titles = [p.get('title') for p in pages[:5] if p.get('title')]
        page_summary += f"Top pages: {', '.join(titles)}."
        
    prompt = f"""
    حلّل بيانات SEO هذه وأعد خطة عمل باللغة العربية.
    العلامة التجارية: {brand}
    {page_summary}
    حالة الظهور في AI: {'جيد' if ai_vis.get('results') else 'ضعيف / غير موجود'}
    
    اكتب أهم 6 إجراءات استراتيجية بالعربية لتحسين SEO والظهور في الذكاء الاصطناعي.
    
    الصيغة المطلوبة حصراً:
    {{
      "actions": [
        {{
          "type": "content|تقني|سلطة|تواصل",
          "task": "وصف قصير واضح بالعربية",
          "priority": "high|medium|low"
        }}
      ]
    }}
    """
    
    # 3. AI Decision Layer
    raw_ai_response = ""
    # Try Groq first (fastest/cheapest usually)
    groq_key = api_keys.get('groq') or os.getenv('GROQ_API_KEY')
    if groq_key:
        raw_ai_response = _call_groq(prompt, groq_key)
        
    # Try OpenAI
    if not raw_ai_response:
        openai_key = api_keys.get('openai') or os.getenv('OPENAI_API_KEY')
        if openai_key:
            raw_ai_response = _call_openai(prompt, openai_key)
            
    # Try Ollama (Local fallback)
    if not raw_ai_response:
        raw_ai_response = _call_ollama(prompt)
        
    # Parse AI response
    ai_actions = []
    if raw_ai_response:
        try:
            parsed = json.loads(raw_ai_response)
            if "actions" in parsed:
                ai_actions = parsed.get("actions", [])
        except Exception as e:
            print(f"Failed to parse AI action response: {e}")
            
    # Combine
    combined_actions = rule_actions + ai_actions
    
    # Phase 4: Build Entity Graph and Link Actions
    from server import entity_extractor
    try:
        job_id = audit_data.get('id')
        entity_extractor.build_knowledge_graph(job_id, audit_data)
    except Exception as e:
        print(f"Entity Graph Error: {e}")

    # Deduplicate loosely by task string
    unique_actions = []
    seen = set()
    for act in combined_actions:
        
        t = act.get('task', '').strip().lower()
        if t not in seen and t:
            seen.add(t)
            unique_actions.append(act)
            
    return {
        "ok": True,
        "actions": unique_actions
    }